diff options
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/BooleanSearch.php | 55 | ||||
| -rw-r--r-- | app/Models/CategoryDAO.php | 26 | ||||
| -rw-r--r-- | app/Models/DatabaseDAO.php | 2 | ||||
| -rw-r--r-- | app/Models/Entry.php | 3 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 188 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 78 | ||||
| -rw-r--r-- | app/Models/Factory.php | 8 | ||||
| -rw-r--r-- | app/Models/Feed.php | 65 | ||||
| -rw-r--r-- | app/Models/FeedDAO.php | 80 | ||||
| -rw-r--r-- | app/Models/FeedDAOSQLite.php | 17 | ||||
| -rw-r--r-- | app/Models/UserQuery.php | 2 |
11 files changed, 353 insertions, 171 deletions
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php new file mode 100644 index 000000000..6e016f7e9 --- /dev/null +++ b/app/Models/BooleanSearch.php @@ -0,0 +1,55 @@ +<?php + +/** + * Contains Boolean search from the search form. + */ +class FreshRSS_BooleanSearch { + + private $raw_input = ''; + private $searches = array(); + + public function __construct($input) { + $input = trim($input); + if ($input == '') { + return; + } + $this->raw_input = $input; + + $input = preg_replace('/:"(.*?)"/', ':"\1"', $input); + $splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE); + + $segment = ''; + $ns = count($splits); + for ($i = 0; $i < $ns; $i++) { + $segment = $segment . $splits[$i]; + if (trim($segment) == '' || strcasecmp($segment, 'OR') === 0) { + $segment = ''; + } else { + $quotes = substr_count($segment, '"') + substr_count($segment, '"'); + if ($quotes % 2 === 0) { + $segment = trim($segment); + if ($segment != '') { + $this->searches[] = new FreshRSS_Search($segment); + } + $segment = ''; + } + } + } + $segment = trim($segment); + if ($segment != '') { + $this->searches[] = new FreshRSS_Search($segment); + } + } + + public function searches() { + return $this->searches; + } + + public function __toString() { + return $this->getRawInput(); + } + + public function getRawInput() { + return $this->raw_input; + } +} diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 68db17db3..ef2c402a0 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -134,7 +134,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable if (isset($cat[0])) { return $cat[0]; } else { - return false; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n"); + } + Minz_Log::error('FreshRSS database error: Default category not found!'); + return null; } } public function checkDefault() { @@ -144,13 +148,27 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable $cat = new FreshRSS_Category(_t('gen.short.default_category')); $cat->_id(self::DEFAULTCATEGORYID); + $sql = 'INSERT INTO `' . $this->prefix . 'category`(id, name) VALUES(?, ?)'; + if (parent::$sharedDbType === 'pgsql') { + //Force call to nextval() + $sql .= " RETURNING nextval('" . $this->prefix . "category_id_seq');"; + } + $stm = $this->bd->prepare($sql); + $values = array( - 'id' => $cat->id(), - 'name' => $cat->name(), + $cat->id(), + $cat->name(), ); - $this->addCategory($values); + if ($stm && $stm->execute($values)) { + return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"'); + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error check default category: ' . json_encode($info)); + return false; + } } + return true; } public function count() { diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php index f5469f2b7..b8e5577e4 100644 --- a/app/Models/DatabaseDAO.php +++ b/app/Models/DatabaseDAO.php @@ -50,7 +50,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { public function feedIsCorrect() { return $this->checkTable('feed', array( 'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate', - 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', + 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes', 'cache_nbEntries', 'cache_nbUnreads' )); } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 0ad3781e5..c6b26a7cc 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -193,7 +193,8 @@ class FreshRSS_Entry extends Minz_Model { try { // l'article n'est pas en BDD, on va le chercher sur le site $this->content = get_content_by_parsing( - htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries + htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries, + $this->feed->attributes() ); } catch (Exception $e) { // rien à faire, on garde l'ancien contenu(requête a échoué) diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 8cdebedc5..a3bca3727 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -437,7 +437,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { * @param integer $priorityMin * @return integer affected rows */ - public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) { + public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) { FreshRSS_UserDAO::touch(); if ($idMax == 0) { $idMax = time() . '000000'; @@ -454,7 +454,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } $values = array($idMax); - list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state); + list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state); $stm = $this->bd->prepare($sql . $search); if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { @@ -480,7 +480,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { * @param integer $idMax fail safe article ID * @return integer affected rows */ - public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) { + public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) { FreshRSS_UserDAO::touch(); if ($idMax == 0) { $idMax = time() . '000000'; @@ -492,7 +492,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?'; $values = array($id, $idMax); - list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state); + list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state); $stm = $this->bd->prepare($sql . $search); if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { @@ -518,7 +518,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { * @param integer $idMax fail safe article ID * @return integer affected rows */ - public function markReadFeed($id_feed, $idMax = 0, $filter = null, $state = 0) { + public function markReadFeed($id_feed, $idMax = 0, $filters = null, $state = 0) { FreshRSS_UserDAO::touch(); if ($idMax == 0) { $idMax = time() . '000000'; @@ -531,7 +531,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'WHERE id_feed=? AND is_read=0 AND id <= ?'; $values = array($id_feed, $idMax); - list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state); + list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state); $stm = $this->bd->prepare($sql . $search); if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { @@ -625,7 +625,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL } - protected function sqlListEntriesWhere($alias = '', $filter = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) { + protected function sqlListEntriesWhere($alias = '', $filters = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) { $search = ' '; $values = array(); if ($state & FreshRSS_Entry::STATE_NOT_READ) { @@ -650,10 +650,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { default: throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!'); } - /*if ($firstId === '' && parent::$sharedDbType === 'mysql') { - //MySQL optimization. TODO: check if this is needed again, after the filtering for old articles has been removed in 0.9-dev - $firstId = $order === 'DESC' ? '9000000000'. '000000' : '0'; - }*/ if ($firstId !== '') { $search .= 'AND ' . $alias . 'id ' . ($order === 'DESC' ? '<=' : '>=') . ' ? '; $values[] = $firstId; @@ -662,91 +658,111 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $search .= 'AND ' . $alias . 'id >= ? '; $values[] = $date_min . '000000'; } - if ($filter) { - if ($filter->getMinDate()) { - $search .= 'AND ' . $alias . 'id >= ? '; - $values[] = "{$filter->getMinDate()}000000"; - } - if ($filter->getMaxDate()) { - $search .= 'AND ' . $alias . 'id <= ? '; - $values[] = "{$filter->getMaxDate()}000000"; - } - if ($filter->getMinPubdate()) { - $search .= 'AND ' . $alias . 'date >= ? '; - $values[] = $filter->getMinPubdate(); - } - if ($filter->getMaxPubdate()) { - $search .= 'AND ' . $alias . 'date <= ? '; - $values[] = $filter->getMaxPubdate(); - } + if ($filters && count($filters->searches()) > 0) { + $isOpen = false; + foreach ($filters->searches() as $filter) { + if ($filter == null) { + continue; + } + $sub_search = ''; + if ($filter->getMinDate()) { + $sub_search .= 'AND ' . $alias . 'id >= ? '; + $values[] = "{$filter->getMinDate()}000000"; + } + if ($filter->getMaxDate()) { + $sub_search .= 'AND ' . $alias . 'id <= ? '; + $values[] = "{$filter->getMaxDate()}000000"; + } + if ($filter->getMinPubdate()) { + $sub_search .= 'AND ' . $alias . 'date >= ? '; + $values[] = $filter->getMinPubdate(); + } + if ($filter->getMaxPubdate()) { + $sub_search .= 'AND ' . $alias . 'date <= ? '; + $values[] = $filter->getMaxPubdate(); + } - if ($filter->getAuthor()) { - foreach ($filter->getAuthor() as $author) { - $search .= 'AND ' . $alias . 'author LIKE ? '; - $values[] = "%{$author}%"; + if ($filter->getAuthor()) { + foreach ($filter->getAuthor() as $author) { + $sub_search .= 'AND ' . $alias . 'author LIKE ? '; + $values[] = "%{$author}%"; + } } - } - if ($filter->getIntitle()) { - foreach ($filter->getIntitle() as $title) { - $search .= 'AND ' . $alias . 'title LIKE ? '; - $values[] = "%{$title}%"; + if ($filter->getIntitle()) { + foreach ($filter->getIntitle() as $title) { + $sub_search .= 'AND ' . $alias . 'title LIKE ? '; + $values[] = "%{$title}%"; + } } - } - if ($filter->getTags()) { - foreach ($filter->getTags() as $tag) { - $search .= 'AND ' . $alias . 'tags LIKE ? '; - $values[] = "%{$tag}%"; + if ($filter->getTags()) { + foreach ($filter->getTags() as $tag) { + $sub_search .= 'AND ' . $alias . 'tags LIKE ? '; + $values[] = "%{$tag}%"; + } } - } - if ($filter->getInurl()) { - foreach ($filter->getInurl() as $url) { - $search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? '; - $values[] = "%{$url}%"; + if ($filter->getInurl()) { + foreach ($filter->getInurl() as $url) { + $sub_search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? '; + $values[] = "%{$url}%"; + } } - } - if ($filter->getNotAuthor()) { - foreach ($filter->getNotAuthor() as $author) { - $search .= 'AND (NOT ' . $alias . 'author LIKE ?) '; - $values[] = "%{$author}%"; + if ($filter->getNotAuthor()) { + foreach ($filter->getNotAuthor() as $author) { + $sub_search .= 'AND (NOT ' . $alias . 'author LIKE ?) '; + $values[] = "%{$author}%"; + } } - } - if ($filter->getNotIntitle()) { - foreach ($filter->getNotIntitle() as $title) { - $search .= 'AND (NOT ' . $alias . 'title LIKE ?) '; - $values[] = "%{$title}%"; + if ($filter->getNotIntitle()) { + foreach ($filter->getNotIntitle() as $title) { + $sub_search .= 'AND (NOT ' . $alias . 'title LIKE ?) '; + $values[] = "%{$title}%"; + } } - } - if ($filter->getNotTags()) { - foreach ($filter->getNotTags() as $tag) { - $search .= 'AND (NOT ' . $alias . 'tags LIKE ?) '; - $values[] = "%{$tag}%"; + if ($filter->getNotTags()) { + foreach ($filter->getNotTags() as $tag) { + $sub_search .= 'AND (NOT ' . $alias . 'tags LIKE ?) '; + $values[] = "%{$tag}%"; + } } - } - if ($filter->getNotInurl()) { - foreach ($filter->getNotInurl() as $url) { - $search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) '; - $values[] = "%{$url}%"; + if ($filter->getNotInurl()) { + foreach ($filter->getNotInurl() as $url) { + $sub_search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) '; + $values[] = "%{$url}%"; + } } - } - if ($filter->getSearch()) { - foreach ($filter->getSearch() as $search_value) { - $search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? '; - $values[] = "%{$search_value}%"; + if ($filter->getSearch()) { + foreach ($filter->getSearch() as $search_value) { + $sub_search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? '; + $values[] = "%{$search_value}%"; + } } - } - if ($filter->getNotSearch()) { - foreach ($filter->getNotSearch() as $search_value) { - $search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) '; - $values[] = "%{$search_value}%"; + if ($filter->getNotSearch()) { + foreach ($filter->getNotSearch() as $search_value) { + $sub_search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) '; + $values[] = "%{$search_value}%"; + } } + + if ($sub_search != '') { + if ($isOpen) { + $search .= 'OR '; + } else { + $search .= 'AND ('; + $isOpen = true; + } + $search .= '(' . substr($sub_search, 4) . ') '; + } + } + if ($isOpen) { + $search .= ') '; } } return array($values, $search); } - private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { + private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) { if (!$state) { $state = FreshRSS_Entry::STATE_ALL; } @@ -777,7 +793,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!'); } - list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state, $order, $firstId, $date_min); + list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min); return array(array_merge($values, $searchValues), 'SELECT e.id FROM `' . $this->prefix . 'entry` e ' @@ -785,11 +801,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'WHERE ' . $where . $search . 'ORDER BY e.id ' . $order - . ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ + . ($limit > 0 ? ' LIMIT ' . intval($limit) : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ } - public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { - list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min); + public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) { + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min); $sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, ' . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') @@ -805,8 +821,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $stm; } - public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { - $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filter, $date_min); + public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) { + $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min); return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC)); } @@ -827,8 +843,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC)); } - public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { //For API - list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min); + public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) { //For API + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min); $stm = $this->bd->prepare($sql); $stm->execute($values); diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 0f57dc1ba..cca970e36 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -7,7 +7,6 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } protected function autoUpdateDb($errorInfo) { - Minz_Log::error('FreshRSS_EntryDAO::autoUpdateDb error: ' . print_r($errorInfo, true)); if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) { $showCreate = $tableInfo->fetchColumn(); if (stripos($showCreate, 'entrytmp') === false) { @@ -27,63 +26,28 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { public function commitNewEntries() { $sql = ' - CREATE TEMP TABLE `tmp` AS - SELECT - id, - guid, - title, - author, - content, - link, - date, - `lastSeen`, - hash, is_read, - is_favorite, - id_feed, - tags - FROM `' . $this->prefix . 'entrytmp` - ORDER BY date; - INSERT OR IGNORE INTO `' . $this->prefix . 'entry` - ( - id, - guid, - title, - author, - content, - link, - date, - `lastSeen`, - hash, - is_read, - is_favorite, - id_feed, - tags - ) - SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS - id, - guid, - title, - author, - content, - link, - date, - `lastSeen`, - hash, - is_read, - is_favorite, - id_feed, - tags - FROM `tmp` - ORDER BY date; - DELETE FROM `' . $this->prefix . 'entrytmp` - WHERE id <= (SELECT MAX(id) - FROM `tmp`); - DROP TABLE `tmp`;'; +DROP TABLE IF EXISTS `tmp`; +CREATE TEMP TABLE `tmp` AS + SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags + FROM `' . $this->prefix . 'entrytmp` + ORDER BY date; +INSERT OR IGNORE INTO `' . $this->prefix . 'entry` + (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) + SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id, + guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags + FROM `tmp` + ORDER BY date; +DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`); +DROP TABLE IF EXISTS `tmp`; +'; $hadTransaction = $this->bd->inTransaction(); if (!$hadTransaction) { $this->bd->beginTransaction(); } $result = $this->bd->exec($sql) !== false; + if (!$result) { + Minz_Log::error('SQL error commitNewEntries: ' . json_encode($this->bd->errorInfo())); + } if (!$hadTransaction) { $this->bd->commit(); } @@ -195,7 +159,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { * @param integer $priorityMin * @return integer affected rows */ - public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) { + public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) { if ($idMax == 0) { $idMax = time() . '000000'; Minz_Log::debug('Calling markReadEntries(0) is deprecated!'); @@ -209,7 +173,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } $values = array($idMax); - list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state); + list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state); $stm = $this->bd->prepare($sql . $search); if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { @@ -235,7 +199,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { * @param integer $idMax fail safe article ID * @return integer affected rows */ - public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) { + public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) { if ($idMax == 0) { $idMax = time() . '000000'; Minz_Log::debug('Calling markReadCat(0) is deprecated!'); @@ -247,7 +211,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { . 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)'; $values = array($idMax, $id); - list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state); + list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state); $stm = $this->bd->prepare($sql . $search); if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { diff --git a/app/Models/Factory.php b/app/Models/Factory.php index dfccc883e..764987c46 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -3,7 +3,13 @@ class FreshRSS_Factory { public static function createFeedDao($username = null) { - return new FreshRSS_FeedDAO($username); + $conf = Minz_Configuration::get('system'); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_FeedDAOSQLite($username); + default: + return new FreshRSS_FeedDAO($username); + } } public static function createEntryDao($username = null) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 196d94931..7eb079f15 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -26,6 +26,7 @@ class FreshRSS_Feed extends Minz_Model { private $error = false; private $keep_history = self::KEEP_HISTORY_DEFAULT; private $ttl = self::TTL_DEFAULT; + private $attributes = array(); private $mute = false; private $hash = null; private $lockPath = ''; @@ -114,6 +115,13 @@ class FreshRSS_Feed extends Minz_Model { public function ttl() { return $this->ttl; } + public function attributes($key = '') { + if ($key == '') { + return $this->attributes; + } else { + return isset($this->attributes[$key]) ? $this->attributes[$key] : null; + } + } public function mute() { return $this->mute; } @@ -234,6 +242,22 @@ class FreshRSS_Feed extends Minz_Model { $this->ttl = abs($value); $this->mute = $value < self::TTL_DEFAULT; } + + public function _attributes($key, $value) { + if ($key == '') { + if (is_string($value)) { + $value = json_decode($value, true); + } + if (is_array($value)) { + $this->attributes = $value; + } + } elseif ($value === null) { + unset($this->attributes[$key]); + } else { + $this->attributes[$key] = $value; + } + } + public function _nbNotRead($value) { $this->nbNotRead = intval($value); } @@ -253,7 +277,7 @@ class FreshRSS_Feed extends Minz_Model { if ($this->httpAuth != '') { $url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url); } - $feed = customSimplePie(); + $feed = customSimplePie($this->attributes()); if (substr($url, -11) === '#force_feed') { $feed->force_feed(true); $url = substr($url, 0, -11); @@ -311,6 +335,8 @@ class FreshRSS_Feed extends Minz_Model { public function loadEntries($feed) { $entries = array(); + $guids = array(); + $hasUniqueGuids = true; foreach ($feed->get_items() as $item) { $title = html_only_entity_decode(strip_tags($item->get_title())); @@ -351,9 +377,13 @@ class FreshRSS_Feed extends Minz_Model { } } + $guid = $item->get_id(false, false); + $hasUniqueGuids &= empty($guids['_' . $guid]); + $guids['_' . $guid] = true; + $entry = new FreshRSS_Entry( $this->id(), - $item->get_id(false, false), + $guid, $title === null ? '' : $title, $author === null ? '' : html_only_entity_decode(strip_tags($author->name == null ? $author->email : $author->name)), $content === null ? '' : $content, @@ -368,14 +398,31 @@ class FreshRSS_Feed extends Minz_Model { unset($item); } + $hasBadGuids = $this->attributes('hasBadGuids'); + if ($hasBadGuids != !$hasUniqueGuids) { + $hasBadGuids = !$hasUniqueGuids; + if ($hasBadGuids) { + Minz_Log::warning('Feed has invalid GUIDs: ' . $this->url); + } else { + Minz_Log::warning('Feed has valid GUIDs again: ' . $this->url); + } + $feedDAO = FreshRSS_Factory::createFeedDao(); + $feedDAO->updateFeedAttribute($this, 'hasBadGuids', $hasBadGuids); + } + if (!$hasUniqueGuids) { + foreach ($entries as $entry) { + $entry->_guid(''); + } + } + $this->entries = $entries; } - function cacheModifiedTime() { + public function cacheModifiedTime() { return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc'); } - function lock() { + public function lock() { $this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock'; if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) { @unlink($this->lockPath); @@ -388,13 +435,13 @@ class FreshRSS_Feed extends Minz_Model { return true; } - function unlock() { + public function unlock() { @unlink($this->lockPath); } //<PubSubHubbub> - function pubSubHubbubEnabled() { + public function pubSubHubbubEnabled() { $url = $this->selfUrl ? $this->selfUrl : $this->url; $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json'; if ($hubFile = @file_get_contents($hubFilename)) { @@ -407,7 +454,7 @@ class FreshRSS_Feed extends Minz_Model { return false; } - function pubSubHubbubError($error = true) { + public 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); @@ -420,7 +467,7 @@ class FreshRSS_Feed extends Minz_Model { return false; } - function pubSubHubbubPrepare() { + public 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); @@ -467,7 +514,7 @@ class FreshRSS_Feed extends Minz_Model { } //Parameter true to subscribe, false to unsubscribe. - function pubSubHubbubSubscribe($state) { + public function pubSubHubbubSubscribe($state) { $url = $this->selfUrl ? $this->selfUrl : $this->url; if (FreshRSS_Context::$system_conf->base_url && $url) { $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json'; diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 0c25ab0ba..9d980c139 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -1,6 +1,33 @@ <?php class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { + + protected function addColumn($name) { + Minz_Log::warning('FreshRSS_FeedDAO::addColumn: ' . $name); + try { + if ($name === 'attributes') { //v1.11.0 + $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN attributes TEXT'); + return $stm && $stm->execute(); + } + } catch (Exception $e) { + Minz_Log::error('FreshRSS_FeedDAO::addColumn error: ' . $e->getMessage()); + } + return false; + } + + protected function autoUpdateDb($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] === '42S22' || $errorInfo[0] === '42703') { //ER_BAD_FIELD_ERROR (Mysql), undefined_column (PostgreSQL) + foreach (array('attributes') as $column) { + if (stripos($errorInfo[2], $column) !== false) { + return $this->addColumn($column); + } + } + } + } + return false; + } + public function addFeed($valuesTmp) { $sql = ' INSERT INTO `' . $this->prefix . 'feed` @@ -15,10 +42,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { `httpAuth`, error, keep_history, - ttl + ttl, + attributes ) VALUES - (?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?)'; + (?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?, ?)'; $stm = $this->bd->prepare($sql); $valuesTmp['url'] = safe_ascii($valuesTmp['url']); @@ -34,12 +62,16 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { base64_encode($valuesTmp['httpAuth']), FreshRSS_Feed::KEEP_HISTORY_DEFAULT, FreshRSS_Feed::TTL_DEFAULT, + isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '', ); if ($stm && $stm->execute($values)) { return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"'); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->addFeed($valuesTmp); + } Minz_Log::error('SQL error addFeed: ' . $info[2]); return false; } @@ -60,7 +92,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 'website' => $feed->website(), 'description' => $feed->description(), 'lastUpdate' => 0, - 'httpAuth' => $feed->httpAuth() + 'httpAuth' => $feed->httpAuth(), + 'attributes' => $feed->attributes(), ); $id = $this->addFeed($values); @@ -87,8 +120,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { foreach ($valuesTmp as $key => $v) { $set .= '`' . $key . '`=?, '; - if ($key == 'httpAuth') { + if ($key === 'httpAuth') { $valuesTmp[$key] = base64_encode($v); + } elseif ($key === 'attributes') { + $valuesTmp[$key] = json_encode($v); } } $set = substr($set, 0, -2); @@ -105,11 +140,25 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::error('SQL error updateFeed: ' . $info[2]); + if ($this->autoUpdateDb($info)) { + return $this->updateFeed($id, $valuesTmp); + } + Minz_Log::error('SQL error updateFeed: ' . $info[2] . ' for feed ' . $id); return false; } } + public function updateFeedAttribute($feed, $key, $value) { + if ($feed instanceof FreshRSS_Feed) { + $feed->_attributes($key, $value); + return $this->updateFeed( + $feed->id(), + array('attributes' => $feed->attributes()) + ); + } + return false; + } + public function updateLastUpdate($id, $inError = false, $mtime = 0) { //See also updateCachedValue() $sql = 'UPDATE `' . $this->prefix . 'feed` ' . 'SET `lastUpdate`=?, error=? ' @@ -250,17 +299,25 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { /** * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL. */ - public function listFeedsOrderUpdate($defaultCacheDuration = 3600) { + public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) { $this->updateTTL(); - $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl ' + $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes ' . 'FROM `' . $this->prefix . 'feed` ' . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT . ' AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ') - . 'ORDER BY `lastUpdate`'; + . 'ORDER BY `lastUpdate` ' + . ($limit < 1 ? '' : 'LIMIT ' . intval($limit)); $stm = $this->bd->prepare($sql); - $stm->execute(); - - return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); + if ($stm && $stm->execute()) { + return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); + } else { + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->listFeedsOrderUpdate($defaultCacheDuration); + } + Minz_Log::error('SQL error listFeedsOrderUpdate: ' . $info[2]); + return array(); + } } public function listByCategory($cat) { @@ -385,6 +442,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $myFeed->_error(isset($dao['error']) ? $dao['error'] : 0); $myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : FreshRSS_Feed::KEEP_HISTORY_DEFAULT); $myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT); + $myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : ''); $myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0); $myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0); if (isset($dao['id'])) { diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php new file mode 100644 index 000000000..3c203b378 --- /dev/null +++ b/app/Models/FeedDAOSQLite.php @@ -0,0 +1,17 @@ +<?php + +class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO { + + protected function autoUpdateDb($errorInfo) { + if ($tableInfo = $this->bd->query("PRAGMA table_info('feed')")) { + $columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1); + foreach (array('attributes') as $column) { + if (!in_array($column, $columns)) { + return $this->addColumn($column); + } + } + } + return false; + } + +} diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php index 52747f538..ef94fdaf6 100644 --- a/app/Models/UserQuery.php +++ b/app/Models/UserQuery.php @@ -41,7 +41,7 @@ class FreshRSS_UserQuery { $query['search'] = ''; } // linked to deeply with the search object, need to use dependency injection - $this->search = new FreshRSS_Search($query['search']); + $this->search = new FreshRSS_BooleanSearch($query['search']); if (isset($query['state'])) { $this->state = $query['state']; } |
