aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/Category.php7
-rw-r--r--app/Models/CategoryDAO.php4
-rw-r--r--app/Models/ConfigurationSetter.php31
-rw-r--r--app/Models/Context.php16
-rw-r--r--app/Models/Entry.php20
-rw-r--r--app/Models/EntryDAO.php314
-rw-r--r--app/Models/EntryDAOSQLite.php15
-rw-r--r--app/Models/Feed.php165
-rw-r--r--app/Models/FeedDAO.php21
-rw-r--r--app/Models/LogDAO.php5
-rw-r--r--app/Models/Search.php229
-rw-r--r--app/Models/Searchable.php6
-rw-r--r--app/Models/Share.php2
-rw-r--r--app/Models/UserQuery.php226
14 files changed, 929 insertions, 132 deletions
diff --git a/app/Models/Category.php b/app/Models/Category.php
index 37cb44dc3..9a44a2d09 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -6,6 +6,7 @@ class FreshRSS_Category extends Minz_Model {
private $nbFeed = -1;
private $nbNotRead = -1;
private $feeds = null;
+ private $hasFeedsWithError = false;
public function __construct($name = '', $feeds = null) {
$this->_name($name);
@@ -16,6 +17,7 @@ class FreshRSS_Category extends Minz_Model {
foreach ($feeds as $feed) {
$this->nbFeed++;
$this->nbNotRead += $feed->nbNotRead();
+ $this->hasFeedsWithError |= $feed->inError();
}
}
}
@@ -51,12 +53,17 @@ class FreshRSS_Category extends Minz_Model {
foreach ($this->feeds as $feed) {
$this->nbFeed++;
$this->nbNotRead += $feed->nbNotRead();
+ $this->hasFeedsWithError |= $feed->inError();
}
}
return $this->feeds;
}
+ public function hasFeedsWithError() {
+ return $this->hasFeedsWithError;
+ }
+
public function _id($value) {
$this->id = $value;
}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index 27a558522..b5abac519 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -1,6 +1,6 @@
<?php
-class FreshRSS_CategoryDAO extends Minz_ModelPdo {
+class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function addCategory($valuesTmp) {
$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)';
$stm = $this->bd->prepare($sql);
@@ -13,7 +13,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
return $this->bd->lastInsertId();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
- Minz_Log::error('SQL error addCategory: ' . $info[2] );
+ Minz_Log::error('SQL error addCategory: ' . $info[2]);
return false;
}
}
diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php
index eeb1f2f4c..250c14c39 100644
--- a/app/Models/ConfigurationSetter.php
+++ b/app/Models/ConfigurationSetter.php
@@ -117,12 +117,11 @@ class FreshRSS_ConfigurationSetter {
private function _queries(&$data, $values) {
$data['queries'] = array();
foreach ($values as $value) {
- $value = array_filter($value);
- $params = $value;
- unset($params['name']);
- unset($params['url']);
- $value['url'] = Minz_Url::display(array('params' => $params));
- $data['queries'][] = $value;
+ if ($value instanceof FreshRSS_UserQuery) {
+ $data['queries'][] = $value->toArray();
+ } elseif (is_array($value)) {
+ $data['queries'][] = $value;
+ }
}
}
@@ -192,6 +191,10 @@ class FreshRSS_ConfigurationSetter {
$data['auto_remove_article'] = $this->handleBool($value);
}
+ private function _mark_updated_article_unread(&$data, $value) {
+ $data['mark_updated_article_unread'] = $this->handleBool($value);
+ }
+
private function _display_categories(&$data, $value) {
$data['display_categories'] = $this->handleBool($value);
}
@@ -351,6 +354,9 @@ class FreshRSS_ConfigurationSetter {
'min' => 0,
'max' => $max_small_int,
),
+ 'max_registrations' => array(
+ 'min' => 0,
+ ),
);
foreach ($values as $key => $value) {
@@ -358,10 +364,11 @@ class FreshRSS_ConfigurationSetter {
continue;
}
+ $value = intval($value);
$limits = $limits_keys[$key];
if (
- (!isset($limits['min']) || $value > $limits['min']) &&
- (!isset($limits['max']) || $value < $limits['max'])
+ (!isset($limits['min']) || $value >= $limits['min']) &&
+ (!isset($limits['max']) || $value <= $limits['max'])
) {
$data['limits'][$key] = $value;
}
@@ -371,4 +378,12 @@ class FreshRSS_ConfigurationSetter {
private function _unsafe_autologin_enabled(&$data, $value) {
$data['unsafe_autologin_enabled'] = $this->handleBool($value);
}
+
+ private function _auto_update_url(&$data, $value) {
+ if (!$value) {
+ return;
+ }
+
+ $data['auto_update_url'] = $value;
+ }
}
diff --git a/app/Models/Context.php b/app/Models/Context.php
index 1c770c756..2a58bd4ba 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -10,6 +10,7 @@ class FreshRSS_Context {
public static $categories = array();
public static $name = '';
+ public static $description = '';
public static $total_unread = 0;
public static $total_starred = array(
@@ -30,7 +31,7 @@ class FreshRSS_Context {
public static $state = 0;
public static $order = 'DESC';
public static $number = 0;
- public static $search = '';
+ public static $search;
public static $first_id = '';
public static $next_id = '';
public static $id_max = '';
@@ -94,6 +95,13 @@ class FreshRSS_Context {
}
/**
+ * Return true if the current request targets a feed (and not a category or all articles), false otherwise.
+ */
+ public static function isFeed() {
+ return self::$current_get['feed'] != false;
+ }
+
+ /**
* Return true if $get parameter correspond to the $current_get attribute.
*/
public static function isCurrentGet($get) {
@@ -146,8 +154,8 @@ class FreshRSS_Context {
self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE;
break;
case 'f':
- // We try to find the corresponding feed.
- $feed = FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
+ // We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
+ $feed = FreshRSS_Context::$system_conf->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
@@ -160,6 +168,7 @@ class FreshRSS_Context {
self::$current_get['feed'] = $id;
self::$current_get['category'] = $feed->category();
self::$name = $feed->name();
+ self::$description = $feed->description();
self::$get_unread = $feed->nbNotRead();
break;
case 'c':
@@ -301,4 +310,5 @@ class FreshRSS_Context {
}
return false;
}
+
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 346c98a92..a562a963a 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -14,7 +14,8 @@ class FreshRSS_Entry extends Minz_Model {
private $content;
private $link;
private $date;
- private $is_read;
+ private $hash = null;
+ private $is_read; //Nullable boolean
private $is_favorite;
private $feed;
private $tags;
@@ -88,6 +89,14 @@ class FreshRSS_Entry extends Minz_Model {
}
}
+ public function hash() {
+ if ($this->hash === null) {
+ //Do not include $this->date because it may be automatically generated when lacking
+ $this->hash = md5($this->link . $this->title . $this->author . $this->content . $this->tags(true));
+ }
+ return $this->hash;
+ }
+
public function _id($value) {
$this->id = $value;
}
@@ -95,23 +104,28 @@ class FreshRSS_Entry extends Minz_Model {
$this->guid = $value;
}
public function _title($value) {
+ $this->hash = null;
$this->title = $value;
}
public function _author($value) {
+ $this->hash = null;
$this->author = $value;
}
public function _content($value) {
+ $this->hash = null;
$this->content = $value;
}
public function _link($value) {
+ $this->hash = null;
$this->link = $value;
}
public function _date($value) {
+ $this->hash = null;
$value = intval($value);
$this->date = $value > 1 ? $value : time();
}
public function _isRead($value) {
- $this->is_read = $value;
+ $this->is_read = $value === null ? null : (bool)$value;
}
public function _isFavorite($value) {
$this->is_favorite = $value;
@@ -120,6 +134,7 @@ class FreshRSS_Entry extends Minz_Model {
$this->feed = $value;
}
public function _tags($value) {
+ $this->hash = null;
if (!is_array($value)) {
$value = array($value);
}
@@ -182,6 +197,7 @@ class FreshRSS_Entry extends Minz_Model {
'content' => $this->content(),
'link' => $this->link(),
'date' => $this->date(true),
+ 'hash' => $this->hash(),
'is_read' => $this->isRead(),
'is_favorite' => $this->isFavorite(),
'id_feed' => $this->feed(),
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 61beeea13..f74055835 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -1,25 +1,78 @@
<?php
-class FreshRSS_EntryDAO extends Minz_ModelPdo {
+class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function isCompressed() {
return parent::$sharedDbType !== 'sqlite';
}
- public function addEntryPrepare() {
- $sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, '
- . ($this->isCompressed() ? 'content_bin' : 'content')
- . ', link, date, is_read, is_favorite, id_feed, tags) '
- . 'VALUES(?, ?, ?, ?, '
- . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
- . ', ?, ?, ?, ?, ?, ?)';
- return $this->bd->prepare($sql);
+ public function hasNativeHex() {
+ return parent::$sharedDbType !== 'sqlite';
+ }
+
+ protected function addColumn($name) {
+ Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn: ' . $name);
+ $hasTransaction = false;
+ try {
+ $stm = null;
+ if ($name === 'lastSeen') { //v1.1.1
+ if (!$this->bd->inTransaction()) {
+ $this->bd->beginTransaction();
+ $hasTransaction = true;
+ }
+ $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN lastSeen INT(11) DEFAULT 0');
+ if ($stm && $stm->execute()) {
+ $stm = $this->bd->prepare('CREATE INDEX entry_lastSeen_index ON `' . $this->prefix . 'entry`(`lastSeen`);'); //"IF NOT EXISTS" does not exist in MySQL 5.7
+ if ($stm && $stm->execute()) {
+ if ($hasTransaction) {
+ $this->bd->commit();
+ }
+ return true;
+ }
+ }
+ if ($hasTransaction) {
+ $this->bd->rollBack();
+ }
+ } elseif ($name === 'hash') { //v1.1.1
+ $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN hash BINARY(16)');
+ return $stm && $stm->execute();
+ }
+ } catch (Exception $e) {
+ Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn error: ' . $e->getMessage());
+ if ($hasTransaction) {
+ $this->bd->rollBack();
+ }
+ }
+ return false;
+ }
+
+ protected function autoAddColumn($errorInfo) {
+ if (isset($errorInfo[0])) {
+ if ($errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR
+ foreach (array('lastSeen', 'hash') as $column) {
+ if (stripos($errorInfo[2], $column) !== false) {
+ return $this->addColumn($column);
+ }
+ }
+ }
+ }
+ return false;
}
- public function addEntry($valuesTmp, $preparedStatement = null) {
- $stm = $preparedStatement === null ?
- FreshRSS_EntryDAO::addEntryPrepare() :
- $preparedStatement;
+ private $addEntryPrepared = null;
+
+ public function addEntry($valuesTmp) {
+ if ($this->addEntryPrepared === null) {
+ $sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, '
+ . ($this->isCompressed() ? 'content_bin' : 'content')
+ . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) '
+ . 'VALUES(?, ?, ?, ?, '
+ . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
+ . ', ?, ?, ?, '
+ . ($this->hasNativeHex() ? 'X?' : '?')
+ . ', ?, ?, ?, ?)';
+ $this->addEntryPrepared = $this->bd->prepare($sql);
+ }
$values = array(
$valuesTmp['id'],
@@ -29,55 +82,76 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
$valuesTmp['content'],
substr($valuesTmp['link'], 0, 1023),
$valuesTmp['date'],
+ time(),
+ $this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']), // X'09AF' hexadecimal literals do not work with SQLite/PDO //hex2bin() is PHP5.4+
$valuesTmp['is_read'] ? 1 : 0,
$valuesTmp['is_favorite'] ? 1 : 0,
$valuesTmp['id_feed'],
substr($valuesTmp['tags'], 0, 1023),
);
- if ($stm && $stm->execute($values)) {
+ if ($this->addEntryPrepared && $this->addEntryPrepared->execute($values)) {
return $this->bd->lastInsertId();
} else {
- $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
- if ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
+ $info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
+ if ($this->autoAddColumn($info)) {
+ return $this->addEntry($valuesTmp);
+ } elseif ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
- . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']);
- } /*else {
- Minz_Log::debug('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
- . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']);
- }*/
+ . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']);
+ }
return false;
}
}
- public function addEntryObject($entry, $conf, $feedHistory) {
- $existingGuids = array_fill_keys(
- $this->listLastGuidsByFeed($entry->feed(), 20), 1
- );
-
- $nb_month_old = max($conf->old_entries, 1);
- $date_min = time() - (3600 * 24 * 30 * $nb_month_old);
-
- $eDate = $entry->date(true);
+ private $updateEntryPrepared = null;
- if ($feedHistory == -2) {
- $feedHistory = $conf->keep_history_default;
+ public function updateEntry($valuesTmp) {
+ if (!isset($valuesTmp['is_read'])) {
+ $valuesTmp['is_read'] = null;
}
- if (!isset($existingGuids[$entry->guid()]) &&
- ($feedHistory != 0 || $eDate >= $date_min || $entry->isFavorite())) {
- $values = $entry->toArray();
-
- $useDeclaredDate = empty($existingGuids);
- $values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
- min(time(), $eDate) . uSecString() :
- uTimeString();
+ if ($this->updateEntryPrepared === null) {
+ $sql = 'UPDATE `' . $this->prefix . 'entry` '
+ . 'SET title=?, author=?, '
+ . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?')
+ . ', link=?, date=?, lastSeen=?, hash='
+ . ($this->hasNativeHex() ? 'X?' : '?')
+ . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ')
+ . 'tags=? '
+ . 'WHERE id_feed=? AND guid=?';
+ $this->updateEntryPrepared = $this->bd->prepare($sql);
+ }
- return $this->addEntry($values);
+ $values = array(
+ substr($valuesTmp['title'], 0, 255),
+ substr($valuesTmp['author'], 0, 255),
+ $valuesTmp['content'],
+ substr($valuesTmp['link'], 0, 1023),
+ $valuesTmp['date'],
+ time(),
+ $this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),
+ );
+ if ($valuesTmp['is_read'] !== null) {
+ $values[] = $valuesTmp['is_read'] ? 1 : 0;
}
+ $values = array_merge($values, array(
+ substr($valuesTmp['tags'], 0, 1023),
+ $valuesTmp['id_feed'],
+ substr($valuesTmp['guid'], 0, 760),
+ ));
- // We don't return Entry object to avoid a research in DB
- return -1;
+ if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) {
+ return $this->bd->lastInsertId();
+ } else {
+ $info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
+ if ($this->autoAddColumn($info)) {
+ return $this->updateEntry($valuesTmp);
+ }
+ Minz_Log::error('SQL error updateEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
+ . ' while updating entry with GUID ' . $valuesTmp['guid'] . ' in feed ' . $valuesTmp['id_feed']);
+ return false;
+ }
}
/**
@@ -94,6 +168,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
if (!is_array($ids)) {
$ids = array($ids);
}
+ if (count($ids) < 1) {
+ return 0;
+ }
$sql = 'UPDATE `' . $this->prefix . 'entry` '
. 'SET is_favorite=? '
. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
@@ -296,11 +373,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
*
* If $idMax equals 0, a deprecated debug message is logged
*
- * @param integer $id feed ID
+ * @param integer $id_feed feed ID
* @param integer $idMax fail safe article ID
* @return integer affected rows
*/
- public function markReadFeed($id, $idMax = 0) {
+ public function markReadFeed($id_feed, $idMax = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadFeed(0) is deprecated!');
@@ -310,7 +387,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
$sql = 'UPDATE `' . $this->prefix . 'entry` '
. 'SET is_read=1 '
. 'WHERE id_feed=? AND is_read=0 AND id <= ?';
- $values = array($id, $idMax);
+ $values = array($id_feed, $idMax);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
@@ -324,7 +401,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET cache_nbUnreads=cache_nbUnreads-' . $affected
. ' WHERE id=?';
- $values = array($id);
+ $values = array($id_feed);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
@@ -338,7 +415,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
return $affected;
}
- public function searchByGuid($feed_id, $id) {
+ public function searchByGuid($id_feed, $guid) {
// un guid est unique pour un flux donné
$sql = 'SELECT id, guid, title, author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
@@ -347,8 +424,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
$stm = $this->bd->prepare($sql);
$values = array(
- $feed_id,
- $id
+ $id_feed,
+ $guid,
);
$stm->execute($values);
@@ -441,56 +518,50 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
$where .= 'AND e1.id >= ' . $date_min . '000000 ';
}
$search = '';
- if ($filter !== '') {
- require_once(LIB_PATH . '/lib_date.php');
- $filter = trim($filter);
- $filter = addcslashes($filter, '\\%_');
- $terms = array_unique(explode(' ', $filter));
- //sort($terms); //Put #tags first //TODO: Put the cheapest filters first
- foreach ($terms as $word) {
- $word = trim($word);
- if (stripos($word, 'intitle:') === 0) {
- $word = substr($word, strlen('intitle:'));
- $search .= 'AND e1.title LIKE ? ';
- $values[] = '%' . $word .'%';
- } elseif (stripos($word, 'inurl:') === 0) {
- $word = substr($word, strlen('inurl:'));
- $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? ';
- $values[] = '%' . $word .'%';
- } elseif (stripos($word, 'author:') === 0) {
- $word = substr($word, strlen('author:'));
- $search .= 'AND e1.author LIKE ? ';
- $values[] = '%' . $word .'%';
- } elseif (stripos($word, 'date:') === 0) {
- $word = substr($word, strlen('date:'));
- list($minDate, $maxDate) = parseDateInterval($word);
- if ($minDate) {
- $search .= 'AND e1.id >= ' . $minDate . '000000 ';
- }
- if ($maxDate) {
- $search .= 'AND e1.id <= ' . $maxDate . '000000 ';
- }
- } elseif (stripos($word, 'pubdate:') === 0) {
- $word = substr($word, strlen('pubdate:'));
- list($minDate, $maxDate) = parseDateInterval($word);
- if ($minDate) {
- $search .= 'AND e1.date >= ' . $minDate . ' ';
- }
- if ($maxDate) {
- $search .= 'AND e1.date <= ' . $maxDate . ' ';
- }
- } else {
- if ($word[0] === '#' && isset($word[1])) {
- $search .= 'AND e1.tags LIKE ? ';
- $values[] = '%' . $word .'%';
- } else {
- $search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? ';
- $values[] = '%' . $word .'%';
- }
+ if ($filter) {
+ if ($filter->getIntitle()) {
+ $search .= 'AND e1.title LIKE ? ';
+ $values[] = "%{$filter->getIntitle()}%";
+ }
+ if ($filter->getInurl()) {
+ $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? ';
+ $values[] = "%{$filter->getInurl()}%";
+ }
+ if ($filter->getAuthor()) {
+ $search .= 'AND e1.author LIKE ? ';
+ $values[] = "%{$filter->getAuthor()}%";
+ }
+ if ($filter->getMinDate()) {
+ $search .= 'AND e1.id >= ? ';
+ $values[] = "{$filter->getMinDate()}000000";
+ }
+ if ($filter->getMaxDate()) {
+ $search .= 'AND e1.id <= ? ';
+ $values[] = "{$filter->getMaxDate()}000000";
+ }
+ if ($filter->getMinPubdate()) {
+ $search .= 'AND e1.date >= ? ';
+ $values[] = $filter->getMinPubdate();
+ }
+ if ($filter->getMaxPubdate()) {
+ $search .= 'AND e1.date <= ? ';
+ $values[] = $filter->getMaxPubdate();
+ }
+ if ($filter->getTags()) {
+ $tags = $filter->getTags();
+ foreach ($tags as $tag) {
+ $search .= 'AND e1.tags LIKE ? ';
+ $values[] = "%{$tag}%";
+ }
+ }
+ if ($filter->getSearch()) {
+ $search_values = $filter->getSearch();
+ foreach ($search_values as $search_value) {
+ $search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? ';
+ $values[] = "%{$search_value}%";
}
}
}
-
return array($values,
'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 '
. ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed=f.id ' : '')
@@ -527,12 +598,51 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
}
- public function listLastGuidsByFeed($id, $n) {
- $sql = 'SELECT guid FROM `' . $this->prefix . 'entry` WHERE id_feed=? ORDER BY id DESC LIMIT ' . intval($n);
+ public function listHashForFeedGuids($id_feed, $guids) {
+ if (count($guids) < 1) {
+ return array();
+ }
+ $sql = 'SELECT guid, hex(hash) AS hexHash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
$stm = $this->bd->prepare($sql);
- $values = array($id);
- $stm->execute($values);
- return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+ $values = array($id_feed);
+ $values = array_merge($values, $guids);
+ if ($stm && $stm->execute($values)) {
+ $result = array();
+ $rows = $stm->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($rows as $row) {
+ $result[$row['guid']] = $row['hexHash'];
+ }
+ return $result;
+ } else {
+ $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+ if ($this->autoAddColumn($info)) {
+ return $this->listHashForFeedGuids($id_feed, $guids);
+ }
+ Minz_Log::error('SQL error listHashForFeedGuids: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
+ . ' while querying feed ' . $id_feed);
+ return false;
+ }
+ }
+
+ public function updateLastSeen($id_feed, $guids) {
+ if (count($guids) < 1) {
+ return 0;
+ }
+ $sql = 'UPDATE `' . $this->prefix . 'entry` SET lastSeen=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+ $stm = $this->bd->prepare($sql);
+ $values = array(time(), $id_feed);
+ $values = array_merge($values, $guids);
+ if ($stm && $stm->execute($values)) {
+ return $stm->rowCount();
+ } else {
+ $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+ if ($this->autoAddColumn($info)) {
+ return $this->updateLastSeen($id_feed, $guids);
+ }
+ Minz_Log::error('SQL error updateLastSeen: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
+ . ' while updating feed ' . $id_feed);
+ return false;
+ }
}
public function countUnreadRead() {
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index ffe0f037c..ff049d813 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -2,6 +2,21 @@
class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
+ protected function autoAddColumn($errorInfo) {
+ if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR
+ if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
+ $showCreate = $tableInfo->fetchColumn();
+ Minz_Log::debug('FreshRSS_EntryDAOSQLite::autoAddColumn: ' . $showCreate);
+ foreach (array('lastSeen', 'hash') as $column) {
+ if (stripos($showCreate, $column) === false) {
+ return $this->addColumn($column);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
protected function sqlConcat($s1, $s2) {
return $s1 . '||' . $s2;
}
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 5ce03be5d..23491ee8d 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -19,6 +19,8 @@ class FreshRSS_Feed extends Minz_Model {
private $ttl = -2;
private $hash = null;
private $lockPath = '';
+ private $hubUrl = '';
+ private $selfUrl = '';
public function __construct($url, $validate=true) {
if ($validate) {
@@ -49,6 +51,12 @@ class FreshRSS_Feed extends Minz_Model {
public function url() {
return $this->url;
}
+ public function selfUrl() {
+ return $this->selfUrl;
+ }
+ public function hubUrl() {
+ return $this->hubUrl;
+ }
public function category() {
return $this->category;
}
@@ -96,6 +104,16 @@ class FreshRSS_Feed extends Minz_Model {
public function ttl() {
return $this->ttl;
}
+ // public function ttlExpire() {
+ // $ttl = $this->ttl;
+ // if ($ttl == -2) { //Default
+ // $ttl = FreshRSS_Context::$user_conf->ttl_default;
+ // }
+ // if ($ttl == -1) { //Never
+ // $ttl = 64000000; //~2 years. Good enough for PubSubHubbub logic
+ // }
+ // return $this->lastUpdate + $ttl;
+ // }
public function nbEntries() {
if ($this->nbEntries < 0) {
$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -226,6 +244,11 @@ class FreshRSS_Feed extends Minz_Model {
throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Feed error' : $errorMessage) . ' [' . $url . ']');
}
+ $links = $feed->get_links('self');
+ $this->selfUrl = isset($links[0]) ? $links[0] : null;
+ $links = $feed->get_links('hub');
+ $this->hubUrl = isset($links[0]) ? $links[0] : null;
+
if ($loadDetails) {
// si on a utilisé l'auto-discover, notre url va avoir changé
$subscribe_url = $feed->subscribe_url(false);
@@ -240,16 +263,16 @@ class FreshRSS_Feed extends Minz_Model {
$subscribe_url = $feed->subscribe_url(true);
}
- $clean_url = url_remove_credentials($subscribe_url);
+ $clean_url = SimplePie_Misc::url_remove_credentials($subscribe_url);
if ($subscribe_url !== null && $subscribe_url !== $url) {
$this->_url($clean_url);
}
- if (($mtime === true) ||($mtime > $this->lastUpdate)) {
- Minz_Log::notice('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
+ if (($mtime === true) || ($mtime > $this->lastUpdate)) {
+ //Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
$this->loadEntries($feed); // et on charge les articles du flux
} else {
- Minz_Log::notice('FreshRSS use cache for ' . $clean_url);
+ //Minz_Log::debug('FreshRSS use cache for ' . $clean_url);
$this->entries = array();
}
@@ -259,7 +282,7 @@ class FreshRSS_Feed extends Minz_Model {
}
}
- private function loadEntries($feed) {
+ public function loadEntries($feed) {
$entries = array();
foreach ($feed->get_items() as $item) {
@@ -333,4 +356,136 @@ class FreshRSS_Feed extends Minz_Model {
function unlock() {
@unlink($this->lockPath);
}
+
+ //<PubSubHubbub>
+
+ function pubSubHubbubEnabled() {
+ $url = $this->selfUrl ? $this->selfUrl : $this->url;
+ $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
+ if ($hubFile = @file_get_contents($hubFilename)) {
+ $hubJson = json_decode($hubFile, true);
+ if ($hubJson && empty($hubJson['error']) &&
+ (empty($hubJson['lease_end']) || $hubJson['lease_end'] > time())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function pubSubHubbubError($error = true) {
+ $url = $this->selfUrl ? $this->selfUrl : $this->url;
+ $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
+ $hubFile = @file_get_contents($hubFilename);
+ $hubJson = $hubFile ? json_decode($hubFile, true) : array();
+ if (!isset($hubJson['error']) || $hubJson['error'] !== (bool)$error) {
+ $hubJson['error'] = (bool)$error;
+ file_put_contents($hubFilename, json_encode($hubJson));
+ file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t"
+ . 'Set error to ' . ($error ? 1 : 0) . ' for ' . $url . "\n", FILE_APPEND);
+ }
+ return false;
+ }
+
+ function pubSubHubbubPrepare() {
+ $key = '';
+ if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) {
+ $path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl);
+ $hubFilename = $path . '/!hub.json';
+ if ($hubFile = @file_get_contents($hubFilename)) {
+ $hubJson = json_decode($hubFile, true);
+ if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
+ $text = 'Invalid JSON for PubSubHubbub: ' . $this->url;
+ Minz_Log::warning($text);
+ file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+ return false;
+ }
+ if ((!empty($hubJson['lease_end'])) && ($hubJson['lease_end'] < (time() + (3600 * 23)))) { //TODO: Make a better policy
+ $text = 'PubSubHubbub lease ends at '
+ . date('c', empty($hubJson['lease_end']) ? time() : $hubJson['lease_end'])
+ . ' and needs renewal: ' . $this->url;
+ Minz_Log::warning($text);
+ file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+ $key = $hubJson['key']; //To renew our lease
+ } elseif (((!empty($hubJson['error'])) || empty($hubJson['lease_end'])) &&
+ (empty($hubJson['lease_start']) || $hubJson['lease_start'] < time() - (3600 * 23))) { //Do not renew too often
+ $key = $hubJson['key']; //To renew our lease
+ }
+ } else {
+ @mkdir($path, 0777, true);
+ $key = sha1($path . FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true));
+ $hubJson = array(
+ 'hub' => $this->hubUrl,
+ 'key' => $key,
+ );
+ file_put_contents($hubFilename, json_encode($hubJson));
+ @mkdir(PSHB_PATH . '/keys/');
+ file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', base64url_encode($this->selfUrl));
+ $text = 'PubSubHubbub prepared for ' . $this->url;
+ Minz_Log::debug($text);
+ file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+ }
+ $currentUser = Minz_Session::param('currentUser');
+ if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) {
+ touch($path . '/' . $currentUser . '.txt');
+ }
+ }
+ return $key;
+ }
+
+ //Parameter true to subscribe, false to unsubscribe.
+ function pubSubHubbubSubscribe($state) {
+ if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) {
+ $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json';
+ $hubFile = @file_get_contents($hubFilename);
+ if ($hubFile === false) {
+ Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url);
+ return false;
+ }
+ $hubJson = json_decode($hubFile, true);
+ if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
+ Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url);
+ return false;
+ }
+ $callbackUrl = checkUrl(FreshRSS_Context::$system_conf->base_url . 'api/pshb.php?k=' . $hubJson['key']);
+ if ($callbackUrl == '') {
+ Minz_Log::warning('Invalid callback for PubSubHubbub: ' . $this->url);
+ return false;
+ }
+ $ch = curl_init();
+ curl_setopt_array($ch, array(
+ CURLOPT_URL => $this->hubUrl,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
+ CURLOPT_POSTFIELDS => 'hub.verify=sync'
+ . '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
+ . '&hub.topic=' . urlencode($this->selfUrl)
+ . '&hub.callback=' . urlencode($callbackUrl)
+ )
+ );
+ $response = curl_exec($ch);
+ $info = curl_getinfo($ch);
+
+ file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" .
+ 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl .
+ ' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND);
+
+ if (!$state) { //unsubscribe
+ $hubJson['lease_end'] = time() - 60;
+ file_put_contents($hubFilename, json_encode($hubJson));
+ }
+
+ if (substr($info['http_code'], 0, 1) == '2') {
+ return true;
+ } else {
+ $hubJson['lease_start'] = time(); //Prevent trying again too soon
+ $hubJson['error'] = true;
+ file_put_contents($hubFilename, json_encode($hubJson));
+ return false;
+ }
+ }
+ return false;
+ }
+
+ //</PubSubHubbub>
}
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 74597c730..475d39286 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -1,6 +1,6 @@
<?php
-class FreshRSS_FeedDAO extends Minz_ModelPdo {
+class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function addFeed($valuesTmp) {
$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
$stm = $this->bd->prepare($sql);
@@ -322,17 +322,20 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
return $affected;
}
- public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) just after
+ public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) or updateCachedValues() just after
$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
- . 'WHERE id_feed = :id_feed AND id <= :id_max AND is_favorite=0 AND id NOT IN '
- . '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
+ . 'WHERE id_feed=:id_feed AND id<=:id_max '
+ . 'AND is_favorite=0 ' //Do not remove favourites
+ . 'AND lastSeen < (SELECT maxLastSeen FROM (SELECT (MAX(e3.lastSeen)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance
+ . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
$stm = $this->bd->prepare($sql);
- $id_max = intval($date_min) . '000000';
-
- $stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
- $stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
- $stm->bindParam(':keep', $keep, PDO::PARAM_INT);
+ if ($stm) {
+ $id_max = intval($date_min) . '000000';
+ $stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
+ $stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
+ $stm->bindParam(':keep', $keep, PDO::PARAM_INT);
+ }
if ($stm && $stm->execute()) {
return $stm->rowCount();
diff --git a/app/Models/LogDAO.php b/app/Models/LogDAO.php
index 4c56e3150..ab258cd58 100644
--- a/app/Models/LogDAO.php
+++ b/app/Models/LogDAO.php
@@ -21,5 +21,10 @@ class FreshRSS_LogDAO {
public static function truncate() {
file_put_contents(join_path(DATA_PATH, 'users', Minz_Session::param('currentUser', '_'), 'log.txt'), '');
+ if (FreshRSS_Auth::hasAccess('admin')) {
+ file_put_contents(join_path(DATA_PATH, 'users', '_', 'log.txt'), '');
+ file_put_contents(join_path(DATA_PATH, 'users', '_', 'log_api.txt'), '');
+ file_put_contents(join_path(DATA_PATH, 'users', '_', 'log_pshb.txt'), '');
+ }
}
}
diff --git a/app/Models/Search.php b/app/Models/Search.php
new file mode 100644
index 000000000..575a9a2cb
--- /dev/null
+++ b/app/Models/Search.php
@@ -0,0 +1,229 @@
+<?php
+
+require_once(LIB_PATH . '/lib_date.php');
+
+/**
+ * Contains a search from the search form.
+ *
+ * It allows to extract meaningful bits of the search and store them in a
+ * convenient object
+ */
+class FreshRSS_Search {
+
+ // This contains the user input string
+ private $raw_input = '';
+ // The following properties are extracted from the raw input
+ private $intitle;
+ private $min_date;
+ private $max_date;
+ private $min_pubdate;
+ private $max_pubdate;
+ private $inurl;
+ private $author;
+ private $tags;
+ private $search;
+
+ public function __construct($input) {
+ if (strcmp($input, '') == 0) {
+ return;
+ }
+ $this->raw_input = $input;
+ $input = $this->parseIntitleSearch($input);
+ $input = $this->parseAuthorSearch($input);
+ $input = $this->parseInurlSearch($input);
+ $input = $this->parsePubdateSearch($input);
+ $input = $this->parseDateSearch($input);
+ $input = $this->parseTagsSeach($input);
+ $this->parseSearch($input);
+ }
+
+ public function __toString() {
+ return $this->getRawInput();
+ }
+
+ public function getRawInput() {
+ return $this->raw_input;
+ }
+
+ public function getIntitle() {
+ return $this->intitle;
+ }
+
+ public function getMinDate() {
+ return $this->min_date;
+ }
+
+ public function getMaxDate() {
+ return $this->max_date;
+ }
+
+ public function getMinPubdate() {
+ return $this->min_pubdate;
+ }
+
+ public function getMaxPubdate() {
+ return $this->max_pubdate;
+ }
+
+ public function getInurl() {
+ return $this->inurl;
+ }
+
+ public function getAuthor() {
+ return $this->author;
+ }
+
+ public function getTags() {
+ return $this->tags;
+ }
+
+ public function getSearch() {
+ return $this->search;
+ }
+
+ /**
+ * Parse the search string to find intitle keyword and the search related
+ * to it.
+ * The search is the first word following the keyword.
+ *
+ * @param string $input
+ * @return string
+ */
+ private function parseIntitleSearch($input) {
+ if (preg_match('/intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->intitle = $matches['search'];
+ return str_replace($matches[0], '', $input);
+ }
+ if (preg_match('/intitle:(?P<search>\w*)/', $input, $matches)) {
+ $this->intitle = $matches['search'];
+ return str_replace($matches[0], '', $input);
+ }
+ return $input;
+ }
+
+ /**
+ * Parse the search string to find author keyword and the search related
+ * to it.
+ * The search is the first word following the keyword except when using
+ * a delimiter. Supported delimiters are single quote (') and double
+ * quotes (").
+ *
+ * @param string $input
+ * @return string
+ */
+ private function parseAuthorSearch($input) {
+ if (preg_match('/author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->author = $matches['search'];
+ return str_replace($matches[0], '', $input);
+ }
+ if (preg_match('/author:(?P<search>\w*)/', $input, $matches)) {
+ $this->author = $matches['search'];
+ return str_replace($matches[0], '', $input);
+ }
+ return $input;
+ }
+
+ /**
+ * Parse the search string to find inurl keyword and the search related
+ * to it.
+ * The search is the first word following the keyword except.
+ *
+ * @param string $input
+ * @return string
+ */
+ private function parseInurlSearch($input) {
+ if (preg_match('/inurl:(?P<search>[^\s]*)/', $input, $matches)) {
+ $this->inurl = $matches['search'];
+ return str_replace($matches[0], '', $input);
+ }
+ return $input;
+ }
+
+ /**
+ * Parse the search string to find date keyword and the search related
+ * to it.
+ * The search is the first word following the keyword.
+ *
+ * @param string $input
+ * @return string
+ */
+ private function parseDateSearch($input) {
+ if (preg_match('/date:(?P<search>[^\s]*)/', $input, $matches)) {
+ list($this->min_date, $this->max_date) = parseDateInterval($matches['search']);
+ return str_replace($matches[0], '', $input);
+ }
+ return $input;
+ }
+
+ /**
+ * Parse the search string to find pubdate keyword and the search related
+ * to it.
+ * The search is the first word following the keyword.
+ *
+ * @param string $input
+ * @return string
+ */
+ private function parsePubdateSearch($input) {
+ if (preg_match('/pubdate:(?P<search>[^\s]*)/', $input, $matches)) {
+ list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($matches['search']);
+ return str_replace($matches[0], '', $input);
+ }
+ return $input;
+ }
+
+ /**
+ * Parse the search string to find tags keyword (# followed by a word)
+ * and the search related to it.
+ * The search is the first word following the #.
+ *
+ * @param string $input
+ * @return string
+ */
+ private function parseTagsSeach($input) {
+ if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
+ $this->tags = $matches['search'];
+ return str_replace($matches[0], '', $input);
+ }
+ return $input;
+ }
+
+ /**
+ * Parse the search string to find search values.
+ * Every word is a distinct search value, except when using a delimiter.
+ * Supported delimiters are single quote (') and double quotes (").
+ *
+ * @param string $input
+ * @return string
+ */
+ private function parseSearch($input) {
+ $input = $this->cleanSearch($input);
+ if (strcmp($input, '') == 0) {
+ return;
+ }
+ if (preg_match_all('/(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->search = $matches['search'];
+ $input = str_replace($matches[0], '', $input);
+ }
+ $input = $this->cleanSearch($input);
+ if (strcmp($input, '') == 0) {
+ return;
+ }
+ if (is_array($this->search)) {
+ $this->search = array_merge($this->search, explode(' ', $input));
+ } else {
+ $this->search = explode(' ', $input);
+ }
+ }
+
+ /**
+ * Remove all unnecessary spaces in the search
+ *
+ * @param string $input
+ * @return string
+ */
+ private function cleanSearch($input) {
+ $input = preg_replace('/\s+/', ' ', $input);
+ return trim($input);
+ }
+
+}
diff --git a/app/Models/Searchable.php b/app/Models/Searchable.php
new file mode 100644
index 000000000..d5bcea49d
--- /dev/null
+++ b/app/Models/Searchable.php
@@ -0,0 +1,6 @@
+<?php
+
+interface FreshRSS_Searchable {
+
+ public function searchById($id);
+}
diff --git a/app/Models/Share.php b/app/Models/Share.php
index db6feda19..2a05f2ee9 100644
--- a/app/Models/Share.php
+++ b/app/Models/Share.php
@@ -152,7 +152,7 @@ class FreshRSS_Share {
* Return the current name of the share option.
*/
public function name($real = false) {
- if ($real || is_null($this->custom_name)) {
+ if ($real || is_null($this->custom_name) || empty($this->custom_name)) {
return $this->name;
} else {
return $this->custom_name;
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
new file mode 100644
index 000000000..52747f538
--- /dev/null
+++ b/app/Models/UserQuery.php
@@ -0,0 +1,226 @@
+<?php
+
+/**
+ * Contains the description of a user query
+ *
+ * It allows to extract the meaningful bits of the query to be manipulated in an
+ * easy way.
+ */
+class FreshRSS_UserQuery {
+
+ private $deprecated = false;
+ private $get;
+ private $get_name;
+ private $get_type;
+ private $name;
+ private $order;
+ private $search;
+ private $state;
+ private $url;
+ private $feed_dao;
+ private $category_dao;
+
+ /**
+ * @param array $query
+ * @param FreshRSS_Searchable $feed_dao
+ * @param FreshRSS_Searchable $category_dao
+ */
+ public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null) {
+ $this->category_dao = $category_dao;
+ $this->feed_dao = $feed_dao;
+ if (isset($query['get'])) {
+ $this->parseGet($query['get']);
+ }
+ if (isset($query['name'])) {
+ $this->name = $query['name'];
+ }
+ if (isset($query['order'])) {
+ $this->order = $query['order'];
+ }
+ if (!isset($query['search'])) {
+ $query['search'] = '';
+ }
+ // linked to deeply with the search object, need to use dependency injection
+ $this->search = new FreshRSS_Search($query['search']);
+ if (isset($query['state'])) {
+ $this->state = $query['state'];
+ }
+ if (isset($query['url'])) {
+ $this->url = $query['url'];
+ }
+ }
+
+ /**
+ * Convert the current object to an array.
+ *
+ * @return array
+ */
+ public function toArray() {
+ return array_filter(array(
+ 'get' => $this->get,
+ 'name' => $this->name,
+ 'order' => $this->order,
+ 'search' => $this->search->__toString(),
+ 'state' => $this->state,
+ 'url' => $this->url,
+ ));
+ }
+
+ /**
+ * Parse the get parameter in the query string to extract its name and
+ * type
+ *
+ * @param string $get
+ */
+ private function parseGet($get) {
+ $this->get = $get;
+ if (preg_match('/(?P<type>[acfs])(_(?P<id>\d+))?/', $get, $matches)) {
+ switch ($matches['type']) {
+ case 'a':
+ $this->parseAll();
+ break;
+ case 'c':
+ $this->parseCategory($matches['id']);
+ break;
+ case 'f':
+ $this->parseFeed($matches['id']);
+ break;
+ case 's':
+ $this->parseFavorite();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parse the query string when it is an "all" query
+ */
+ private function parseAll() {
+ $this->get_name = 'all';
+ $this->get_type = 'all';
+ }
+
+ /**
+ * Parse the query string when it is a "category" query
+ *
+ * @param integer $id
+ * @throws FreshRSS_DAO_Exception
+ */
+ private function parseCategory($id) {
+ if (is_null($this->category_dao)) {
+ throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery');
+ }
+ $category = $this->category_dao->searchById($id);
+ if ($category) {
+ $this->get_name = $category->name();
+ } else {
+ $this->deprecated = true;
+ }
+ $this->get_type = 'category';
+ }
+
+ /**
+ * Parse the query string when it is a "feed" query
+ *
+ * @param integer $id
+ * @throws FreshRSS_DAO_Exception
+ */
+ private function parseFeed($id) {
+ if (is_null($this->feed_dao)) {
+ throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery');
+ }
+ $feed = $this->feed_dao->searchById($id);
+ if ($feed) {
+ $this->get_name = $feed->name();
+ } else {
+ $this->deprecated = true;
+ }
+ $this->get_type = 'feed';
+ }
+
+ /**
+ * Parse the query string when it is a "favorite" query
+ */
+ private function parseFavorite() {
+ $this->get_name = 'favorite';
+ $this->get_type = 'favorite';
+ }
+
+ /**
+ * Check if the current user query is deprecated.
+ * It is deprecated if the category or the feed used in the query are
+ * not existing.
+ *
+ * @return boolean
+ */
+ public function isDeprecated() {
+ return $this->deprecated;
+ }
+
+ /**
+ * Check if the user query has parameters.
+ * If the type is 'all', it is considered equal to no parameters
+ *
+ * @return boolean
+ */
+ public function hasParameters() {
+ if ($this->get_type === 'all') {
+ return false;
+ }
+ if ($this->hasSearch()) {
+ return true;
+ }
+ if ($this->state) {
+ return true;
+ }
+ if ($this->order) {
+ return true;
+ }
+ if ($this->get) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if there is a search in the search object
+ *
+ * @return boolean
+ */
+ public function hasSearch() {
+ return $this->search->getRawInput() != "";
+ }
+
+ public function getGet() {
+ return $this->get;
+ }
+
+ public function getGetName() {
+ return $this->get_name;
+ }
+
+ public function getGetType() {
+ return $this->get_type;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function getOrder() {
+ return $this->order;
+ }
+
+ public function getSearch() {
+ return $this->search;
+ }
+
+ public function getState() {
+ return $this->state;
+ }
+
+ public function getUrl() {
+ return $this->url;
+ }
+
+}