diff options
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/CategoryDAO.php | 2 | ||||
| -rw-r--r-- | app/Models/ConfigurationSetter.php | 13 | ||||
| -rw-r--r-- | app/Models/Context.php | 3 | ||||
| -rw-r--r-- | app/Models/Entry.php | 20 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 307 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 15 | ||||
| -rw-r--r-- | app/Models/Feed.php | 8 | ||||
| -rw-r--r-- | app/Models/FeedDAO.php | 21 | ||||
| -rw-r--r-- | app/Models/Search.php | 229 | ||||
| -rw-r--r-- | app/Models/Searchable.php | 6 | ||||
| -rw-r--r-- | app/Models/Share.php | 2 | ||||
| -rw-r--r-- | app/Models/UserQuery.php | 226 |
12 files changed, 726 insertions, 126 deletions
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 27a558522..189a5f0e4 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); diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index eeb1f2f4c..4bd29ecb0 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -117,12 +117,9 @@ 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(); + } } } @@ -192,6 +189,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); } diff --git a/app/Models/Context.php b/app/Models/Context.php index 1c770c756..dbdbfaa69 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -30,7 +30,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 = ''; @@ -301,4 +301,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..f939a0fb3 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -1,25 +1,72 @@ <?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); + protected function addColumn($name) { + Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn: ' . $name); + $hasTransaction = false; + try { + $stm = null; + if ($name === 'lastSeen') { //v1.2 + 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.2 + $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->addEntryPrepared = $this->bd->prepare($sql); + } $values = array( $valuesTmp['id'], @@ -29,55 +76,75 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $valuesTmp['content'], substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], + time(), + hex2bin($valuesTmp['hash']), // X'09AF' hexadecimal literals do not work with SQLite/PDO $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']. ' ' . $this->addEntryPrepared); + } return false; } } - public function addEntryObject($entry, $conf, $feedHistory) { - $existingGuids = array_fill_keys( - $this->listLastGuidsByFeed($entry->feed(), 20), 1 - ); + private $updateEntryPrepared = null; - $nb_month_old = max($conf->old_entries, 1); - $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - - $eDate = $entry->date(true); - - 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=?, ' + . ($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(), + hex2bin($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 +161,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 +366,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 +380,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 +394,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 +408,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 +417,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 +511,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 +591,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_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($id); - $stm->execute($values); - return $stm->fetchAll(PDO::FETCH_COLUMN, 0); + $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..85fb173ec 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -240,16 +240,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(); } 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/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; + } + +} |
