From 115612959302fc51c4720b7a954bf8cbe1b14f3b Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 21 Feb 2015 09:08:06 -0500 Subject: Use the search object to get values in the search It is now possible to combine multiple keywords to do a search. The separation of concern is better now since the search extraction is not done in the DAO anymore. At the moment, a multiple keyword search is stored as this. It could be nice to have it rendered differently in the search page to make it more readable. At the moment, there is a problem with search enclosed by ". Same search works well when enclosed by '. --- app/Models/EntryDAO.php | 84 ++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 46 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 61beeea13..0cf4e1367 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -441,54 +441,46 @@ 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 instanceof FreshRSS_Search) { + 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 .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? '; + $values[] = "%{$filter->getSearch()}%"; + } } return array($values, -- cgit v1.2.3 From e897afa7ccb2c625705bce25c003d9cf37179227 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sun, 22 Feb 2015 17:38:33 -0500 Subject: Change test to verify if there is a filter --- app/Models/EntryDAO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 0cf4e1367..d2a8e0fd9 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -441,7 +441,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'AND e1.id >= ' . $date_min . '000000 '; } $search = ''; - if ($filter instanceof FreshRSS_Search) { + if ($filter !== null) { if ($filter->getIntitle()) { $search .= 'AND e1.title LIKE ? '; $values[] = "%{$filter->getIntitle()}%"; -- cgit v1.2.3 From fe24636e0416df4bb3507a0ac8cfe7e27d5b4c90 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Mon, 2 Mar 2015 20:27:15 -0500 Subject: Add missing white space --- app/Models/EntryDAO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index d2a8e0fd9..cf75a02c9 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -459,7 +459,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $values[] = "{$filter->getMinDate()}000000"; } if ($filter->getMaxDate()) { - $search .= 'AND e1.id <= ?'; + $search .= 'AND e1.id <= ? '; $values[] = "{$filter->getMaxDate()}000000"; } if ($filter->getMinPubdate()) { -- cgit v1.2.3 From 5b90e1f4a0057aa78fd7d8d4d748b01676ec9073 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sun, 1 Mar 2015 09:18:06 -0500 Subject: Introduce user queries objects There is now an object to manipulate user queries. It allows to move logic to handle those from the view and the controller in the model. Thus making the view and the controller easier to read. I introduced a new interface to start using dependency injection. There is still some rough edges but we are moving in the right direction. The new object is fully tested but it still need some improvements, for instance, it is still tied to the search object. There might be a better way to do that. --- app/Controllers/configureController.php | 83 +++--------- app/Exceptions/DAOException.php | 5 + app/Models/CategoryDAO.php | 2 +- app/Models/ConfigurationSetter.php | 7 +- app/Models/EntryDAO.php | 2 +- app/Models/FeedDAO.php | 2 +- app/Models/Searchable.php | 6 + app/Models/UserQuery.php | 226 +++++++++++++++++++++++++++++++ app/views/configure/queries.phtml | 47 +++---- tests/app/Models/UserQueryTest.php | 229 ++++++++++++++++++++++++++++++++ 10 files changed, 505 insertions(+), 104 deletions(-) create mode 100644 app/Exceptions/DAOException.php create mode 100644 app/Models/Searchable.php create mode 100644 app/Models/UserQuery.php create mode 100644 tests/app/Models/UserQueryTest.php (limited to 'app/Models/EntryDAO.php') diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 38ccd2b2d..fc92aa0c2 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -241,13 +241,16 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * checking if categories and feeds are still in use. */ public function queriesAction() { + $category_dao = new FreshRSS_CategoryDAO(); + $feed_dao = FreshRSS_Factory::createFeedDao(); if (Minz_Request::isPost()) { - $queries = Minz_Request::param('queries', array()); + $params = Minz_Request::param('queries', array()); - foreach ($queries as $key => $query) { + foreach ($params as $key => $query) { if (!$query['name']) { $query['name'] = _t('conf.query.number', $key + 1); } + $queries[] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao); } FreshRSS_Context::$user_conf->queries = $queries; FreshRSS_Context::$user_conf->save(); @@ -255,62 +258,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController { Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'queries')); } else { - $this->view->query_get = array(); - $cat_dao = new FreshRSS_CategoryDAO(); - $feed_dao = FreshRSS_Factory::createFeedDao(); + $this->view->queries = array(); foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { - if (!isset($query['get'])) { - continue; - } - - switch ($query['get'][0]) { - case 'c': - $category = $cat_dao->searchById(substr($query['get'], 2)); - - $deprecated = true; - $cat_name = ''; - if ($category) { - $cat_name = $category->name(); - $deprecated = false; - } - - $this->view->query_get[$key] = array( - 'type' => 'category', - 'name' => $cat_name, - 'deprecated' => $deprecated, - ); - break; - case 'f': - $feed = $feed_dao->searchById(substr($query['get'], 2)); - - $deprecated = true; - $feed_name = ''; - if ($feed) { - $feed_name = $feed->name(); - $deprecated = false; - } - - $this->view->query_get[$key] = array( - 'type' => 'feed', - 'name' => $feed_name, - 'deprecated' => $deprecated, - ); - break; - case 's': - $this->view->query_get[$key] = array( - 'type' => 'favorite', - 'name' => 'favorite', - 'deprecated' => false, - ); - break; - case 'a': - $this->view->query_get[$key] = array( - 'type' => 'all', - 'name' => 'all', - 'deprecated' => false, - ); - break; - } + $this->view->queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao); } } @@ -325,16 +275,17 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * lean data. */ public function addQueryAction() { - $whitelist = array('get', 'order', 'name', 'search', 'state'); - $queries = FreshRSS_Context::$user_conf->queries; - $query = Minz_Request::params(); - $query['name'] = _t('conf.query.number', count($queries) + 1); - foreach ($query as $key => $value) { - if (!in_array($key, $whitelist)) { - unset($query[$key]); - } + $category_dao = new FreshRSS_CategoryDAO(); + $feed_dao = FreshRSS_Factory::createFeedDao(); + $queries = array(); + foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { + $queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao); } - $queries[] = $query; + $params = Minz_Request::params(); + $params['url'] = Minz_Url::display(array('params' => $params)); + $params['name'] = _t('conf.query.number', count($queries) + 1); + $queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao); + FreshRSS_Context::$user_conf->queries = $queries; FreshRSS_Context::$user_conf->save(); diff --git a/app/Exceptions/DAOException.php b/app/Exceptions/DAOException.php new file mode 100644 index 000000000..6bd8f4ff0 --- /dev/null +++ b/app/Exceptions/DAOException.php @@ -0,0 +1,5 @@ +prefix . 'category`(name) VALUES(?)'; $stm = $this->bd->prepare($sql); diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index eeb1f2f4c..d7689752f 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -117,12 +117,7 @@ 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; + $data['queries'][] = $value->toArray(); } } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index cf75a02c9..b8a1a43b0 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -1,6 +1,6 @@ prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)'; $stm = $this->bd->prepare($sql); 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 @@ +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[acfs])(_(?P\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_DAOException + */ + private function parseCategory($id) { + if (is_null($this->category_dao)) { + throw new FreshRSS_DAOException('Category DAO is not loaded i 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_DAOException + */ + private function parseFeed($id) { + if (is_null($this->feed_dao)) { + throw new FreshRSS_DAOException('Feed DAO is not loaded i 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; + } + +} diff --git a/app/views/configure/queries.phtml b/app/views/configure/queries.phtml index 5f449deb3..69efcf365 100644 --- a/app/views/configure/queries.phtml +++ b/app/views/configure/queries.phtml @@ -6,27 +6,28 @@
- queries as $key => $query) { ?> + queries as $key => $query) { ?>
- "/> - "/> - "/> - "/> + + + + +
- + @@ -35,23 +36,11 @@
- query_get[$key]) && - $this->query_get[$key]['deprecated']); - ?> - - + hasParameters()) { ?>
- + isDeprecated()) { ?>
@@ -60,20 +49,20 @@
    - -
  • + hasSearch()) { ?> +
  • getSearch()->getRawInput()); ?>
  • - -
  • + getState()) { ?> +
  • getState()); ?>
  • - -
  • + getOrder()) { ?> +
  • getOrder())); ?>
  • - -
  • query_get[$key]['type'], $this->query_get[$key]['name']); ?>
  • + getGet()) { ?> +
  • getGetType(), $query->getGetName()); ?>
diff --git a/tests/app/Models/UserQueryTest.php b/tests/app/Models/UserQueryTest.php new file mode 100644 index 000000000..2234be6e1 --- /dev/null +++ b/tests/app/Models/UserQueryTest.php @@ -0,0 +1,229 @@ + 'a'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertEquals('all', $user_query->getGetName()); + $this->assertEquals('all', $user_query->getGetType()); + } + + public function test__construct_whenFavoriteQuery_storesFavoriteParameters() { + $query = array('get' => 's'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertEquals('favorite', $user_query->getGetName()); + $this->assertEquals('favorite', $user_query->getGetType()); + } + + /** + * @expectedException Exceptions/FreshRSS_DAOException + * @expectedExceptionMessage Category DAO is not loaded in UserQuery + */ + public function test__construct_whenCategoryQueryAndNoDao_throwsException() { + $this->markTestIncomplete('There is a problem with the exception autoloading. We need to make a better autoloading process'); + $query = array('get' => 'c_1'); + new FreshRSS_UserQuery($query); + } + + public function test__construct_whenCategoryQuery_storesCategoryParameters() { + $category_name = 'some category name'; + $cat = $this->getMock('FreshRSS_Category'); + $cat->expects($this->atLeastOnce()) + ->method('name') + ->withAnyParameters() + ->willReturn($category_name); + $cat_dao = $this->getMock('FreshRSS_Searchable'); + $cat_dao->expects($this->atLeastOnce()) + ->method('searchById') + ->withAnyParameters() + ->willReturn($cat); + $query = array('get' => 'c_1'); + $user_query = new FreshRSS_UserQuery($query, null, $cat_dao); + $this->assertEquals($category_name, $user_query->getGetName()); + $this->assertEquals('category', $user_query->getGetType()); + } + + /** + * @expectedException Exceptions/FreshRSS_DAOException + * @expectedExceptionMessage Feed DAO is not loaded in UserQuery + */ + public function test__construct_whenFeedQueryAndNoDao_throwsException() { + $this->markTestIncomplete('There is a problem with the exception autoloading. We need to make a better autoloading process'); + $query = array('get' => 'c_1'); + new FreshRSS_UserQuery($query); + } + + public function test__construct_whenFeedQuery_storesFeedParameters() { + $feed_name = 'some feed name'; + $feed = $this->getMock('FreshRSS_Feed', array(), array('', false)); + $feed->expects($this->atLeastOnce()) + ->method('name') + ->withAnyParameters() + ->willReturn($feed_name); + $feed_dao = $this->getMock('FreshRSS_Searchable'); + $feed_dao->expects($this->atLeastOnce()) + ->method('searchById') + ->withAnyParameters() + ->willReturn($feed); + $query = array('get' => 'f_1'); + $user_query = new FreshRSS_UserQuery($query, $feed_dao, null); + $this->assertEquals($feed_name, $user_query->getGetName()); + $this->assertEquals('feed', $user_query->getGetType()); + } + + public function test__construct_whenUnknownQuery_doesStoreParameters() { + $query = array('get' => 'q'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertNull($user_query->getGetName()); + $this->assertNull($user_query->getGetType()); + } + + public function test__construct_whenName_storesName() { + $name = 'some name'; + $query = array('name' => $name); + $user_query = new FreshRSS_UserQuery($query); + $this->assertEquals($name, $user_query->getName()); + } + + public function test__construct_whenOrder_storesOrder() { + $order = 'some order'; + $query = array('order' => $order); + $user_query = new FreshRSS_UserQuery($query); + $this->assertEquals($order, $user_query->getOrder()); + } + + public function test__construct_whenState_storesState() { + $state = 'some state'; + $query = array('state' => $state); + $user_query = new FreshRSS_UserQuery($query); + $this->assertEquals($state, $user_query->getState()); + } + + public function test__construct_whenUrl_storesUrl() { + $url = 'some url'; + $query = array('url' => $url); + $user_query = new FreshRSS_UserQuery($query); + $this->assertEquals($url, $user_query->getUrl()); + } + + public function testToArray_whenNoData_returnsEmptyArray() { + $user_query = new FreshRSS_UserQuery(array()); + $this->assertInternalType('array', $user_query->toArray()); + $this->assertCount(0, $user_query->toArray()); + } + + public function testToArray_whenData_returnsArray() { + $query = array( + 'get' => 's', + 'name' => 'some name', + 'order' => 'some order', + 'search' => 'some search', + 'state' => 'some state', + 'url' => 'some url', + ); + $user_query = new FreshRSS_UserQuery($query); + $this->assertInternalType('array', $user_query->toArray()); + $this->assertCount(6, $user_query->toArray()); + $this->assertEquals($query, $user_query->toArray()); + } + + public function testHasSearch_whenSearch_returnsTrue() { + $query = array( + 'search' => 'some search', + ); + $user_query = new FreshRSS_UserQuery($query); + $this->assertTrue($user_query->hasSearch()); + } + + public function testHasSearch_whenNoSearch_returnsFalse() { + $user_query = new FreshRSS_UserQuery(array()); + $this->assertFalse($user_query->hasSearch()); + } + + public function testHasParameters_whenAllQuery_returnsFalse() { + $query = array('get' => 'a'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertFalse($user_query->hasParameters()); + } + + public function testHasParameters_whenNoParameter_returnsFalse() { + $query = array(); + $user_query = new FreshRSS_UserQuery($query); + $this->assertFalse($user_query->hasParameters()); + } + + public function testHasParameters_whenParameter_returnTrue() { + $query = array('get' => 's'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertTrue($user_query->hasParameters()); + } + + public function testIsDeprecated_whenCategoryExists_returnFalse() { + $cat = $this->getMock('FreshRSS_Category'); + $cat_dao = $this->getMock('FreshRSS_Searchable'); + $cat_dao->expects($this->atLeastOnce()) + ->method('searchById') + ->withAnyParameters() + ->willReturn($cat); + $query = array('get' => 'c_1'); + $user_query = new FreshRSS_UserQuery($query, null, $cat_dao); + $this->assertFalse($user_query->isDeprecated()); + } + + public function testIsDeprecated_whenCategoryDoesNotExist_returnTrue() { + $cat_dao = $this->getMock('FreshRSS_Searchable'); + $cat_dao->expects($this->atLeastOnce()) + ->method('searchById') + ->withAnyParameters() + ->willReturn(null); + $query = array('get' => 'c_1'); + $user_query = new FreshRSS_UserQuery($query, null, $cat_dao); + $this->assertTrue($user_query->isDeprecated()); + } + + public function testIsDeprecated_whenFeedExists_returnFalse() { + $feed = $this->getMock('FreshRSS_Feed', array(), array('', false)); + $feed_dao = $this->getMock('FreshRSS_Searchable'); + $feed_dao->expects($this->atLeastOnce()) + ->method('searchById') + ->withAnyParameters() + ->willReturn($feed); + $query = array('get' => 'f_1'); + $user_query = new FreshRSS_UserQuery($query, $feed_dao, null); + $this->assertFalse($user_query->isDeprecated()); + } + + public function testIsDeprecated_whenFeedDoesNotExist_returnTrue() { + $feed_dao = $this->getMock('FreshRSS_Searchable'); + $feed_dao->expects($this->atLeastOnce()) + ->method('searchById') + ->withAnyParameters() + ->willReturn(null); + $query = array('get' => 'f_1'); + $user_query = new FreshRSS_UserQuery($query, $feed_dao, null); + $this->assertTrue($user_query->isDeprecated()); + } + + public function testIsDeprecated_whenAllQuery_returnFalse() { + $query = array('get' => 'a'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertFalse($user_query->isDeprecated()); + } + + public function testIsDeprecated_whenFavoriteQuery_returnFalse() { + $query = array('get' => 's'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertFalse($user_query->isDeprecated()); + } + + public function testIsDeprecated_whenUnknownQuery_returnFalse() { + $query = array('get' => 'q'); + $user_query = new FreshRSS_UserQuery($query); + $this->assertFalse($user_query->isDeprecated()); + } + +} -- cgit v1.2.3 From 24f6c1eabb4cea941e40307c2f732c0ca384ffd2 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Thu, 5 Mar 2015 06:47:13 -0500 Subject: Fix spacing --- app/Models/CategoryDAO.php | 2 +- app/Models/EntryDAO.php | 2 +- app/Models/FeedDAO.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 4eee226ba..189a5f0e4 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -1,6 +1,6 @@ prefix . 'category`(name) VALUES(?)'; $stm = $this->bd->prepare($sql); diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index b8a1a43b0..9736d5cd3 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -1,6 +1,6 @@ prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)'; $stm = $this->bd->prepare($sql); -- cgit v1.2.3 From 711530a512b370d79b079205ce1f8376174f7f03 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 4 Apr 2015 22:39:31 +0200 Subject: SQL: detection of updates, and preparation for better burge https://github.com/FreshRSS/FreshRSS/issues/798 https://github.com/FreshRSS/FreshRSS/issues/493 SQLite not yet tested. Only MySQL tested so far. --- app/Controllers/feedController.php | 98 ++++++++------ app/Controllers/importExportController.php | 3 +- app/Models/Entry.php | 16 +++ app/Models/EntryDAO.php | 198 +++++++++++++++++++++-------- app/Models/Feed.php | 1 + app/SQL/install.sql.mysql.php | 7 +- app/SQL/install.sql.sqlite.php | 7 +- lib/lib_rss.php | 2 +- 8 files changed, 231 insertions(+), 101 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 6f544d834..08a0257a2 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -145,7 +145,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Call the extension hook $name = $feed->name(); $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); - if (is_null($feed)) { + if ($feed === null) { Minz_Request::bad(_t('feed_not_added', $name), $url_redirect); } @@ -181,7 +181,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Use a shared statement and a transaction to improve a LOT the // performances. - $prepared_statement = $entryDAO->addEntryPrepare(); $feedDAO->beginTransaction(); foreach ($entries as $entry) { // Entries are added without any verification. @@ -190,13 +189,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entry->_isRead($is_read); $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); - if (is_null($entry)) { + if ($entry === null) { // An extension has returned a null value, there is nothing to insert. continue; } $values = $entry->toArray(); - $entryDAO->addEntry($values, $prepared_statement); + $entryDAO->addEntry($values); } $feedDAO->updateLastUpdate($feed->id()); $feedDAO->commit(); @@ -307,7 +306,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->load(false); } catch (FreshRSS_Feed_Exception $e) { Minz_Log::notice($e->getMessage()); - $feedDAO->updateLastUpdate($feed->id(), 1); + $feedDAO->updateLastUpdate($feed->id(), true); $feed->unlock(); continue; } @@ -323,50 +322,69 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // We want chronological order and SimplePie uses reverse order. $entries = array_reverse($feed->entries()); if (count($entries) > 0) { - // For this feed, check last n entry GUIDs already in database. - $existing_guids = array_fill_keys($entryDAO->listLastGuidsByFeed( - $feed->id(), count($entries) + 10 - ), 1); - $use_declared_date = empty($existing_guids); + $newGuids = array(); + foreach ($entries as $entry) { + $newGuids[] = $entry->guid(); + } + // For this feed, check existing GUIDs already in database. + $existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids); + unset($newGuids); + $use_declared_date = empty($existingHashForGuids); + $oldGuids = array(); // Add entries in database if possible. - $prepared_statement = $entryDAO->addEntryPrepare(); - $feedDAO->beginTransaction(); foreach ($entries as $entry) { $entry_date = $entry->date(true); - if (isset($existing_guids[$entry->guid()]) || - ($feed_history == 0 && $entry_date < $date_min)) { - // This entry already exists in DB or should not be added - // considering configuration and date. - continue; - } - - $id = uTimeString(); - if ($use_declared_date || $entry_date < $date_min) { - // Use declared date at first import. - $id = min(time(), $entry_date) . uSecString(); + if (isset($existingHashForGuids[$entry->guid()])) { + $existingHash = $existingHashForGuids[$entry->guid()]; + if (strcasecmp($existingHash, $entry->hash()) === 0 || $existingHash === '00000000000000000000000000000000') { + //This entry already exists and is unchanged. TODO: Remove the test with the zero'ed hash in FreshRSS v1.3 + $oldGuids[] = $entry->guid(); + } else { //This entry already exists but has been updated + Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() . + ', old hash ' . $existingHash . ', new hash ' . $entry->hash()); + $entry->_isRead($is_read); //Reset is_read + if (!$entryDAO->hasTransaction()) { + $entryDAO->beginTransaction(); + } + $entryDAO->updateEntry($entry->toArray()); + } + } elseif ($feed_history == 0 && $entry_date < $date_min) { + // This entry should not be added considering configuration and date. + $oldGuids[] = $entry->guid(); + } else { + $id = uTimeString(); + if ($use_declared_date || $entry_date < $date_min) { + // Use declared date at first import. + $id = min(time(), $entry_date) . uSecString(); + } + + $entry->_id($id); + $entry->_isRead($is_read); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if ($entry === null) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + + if (!$entryDAO->hasTransaction()) { + $entryDAO->beginTransaction(); + } + $entryDAO->addEntry($entry->toArray()); } - - $entry->_id($id); - $entry->_isRead($is_read); - - $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); - if (is_null($entry)) { - // An extension has returned a null value, there is nothing to insert. - continue; - } - - $values = $entry->toArray(); - $entryDAO->addEntry($values, $prepared_statement); } + $entryDAO->updateLastSeen($feed->id(), $oldGuids); } + //TODO: updateLastSeen old GUIDS once in a while, in the case of caching (i.e. the whole feed content has not changed) if ($feed_history >= 0 && rand(0, 30) === 1) { // TODO: move this function in web cron when available (see entry::purge) // Remove old entries once in 30. - if (!$feedDAO->hasTransaction()) { - $feedDAO->beginTransaction(); + if (!$entryDAO->hasTransaction()) { + $entryDAO->beginTransaction(); } + //TODO: more robust system based on entry.lastSeen to avoid cleaning entries that are still published in the RSS feed. $nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, @@ -377,9 +395,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - $feedDAO->updateLastUpdate($feed->id(), 0, $feedDAO->hasTransaction()); - if ($feedDAO->hasTransaction()) { - $feedDAO->commit(); + $feedDAO->updateLastUpdate($feed->id(), 0, $entryDAO->hasTransaction()); + if ($entryDAO->hasTransaction()) { + $entryDAO->commit(); } if ($feed->url() !== $url) { diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 589777b2a..26b163e43 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -361,7 +361,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } // Then, articles are imported. - $prepared_statement = $this->entryDAO->addEntryPrepare(); $this->entryDAO->beginTransaction(); foreach ($article_object['items'] as $item) { if (!isset($article_to_feed[$item['id']])) { @@ -396,7 +395,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } $values = $entry->toArray(); - $id = $this->entryDAO->addEntry($values, $prepared_statement); + $id = $this->entryDAO->addEntry($values); if (!$error && ($id === false)) { $error = true; diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 346c98a92..6931c9f25 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -14,6 +14,7 @@ class FreshRSS_Entry extends Minz_Model { private $content; private $link; private $date; + private $hash = null; private $is_read; private $is_favorite; private $feed; @@ -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,18 +104,23 @@ 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(); } @@ -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 9736d5cd3..5b4b85547 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -6,20 +6,57 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 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 autoAddColumn($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR + $hasTransaction = false; + try { + $stm = null; + if (stripos($errorInfo[2], 'lastSeen') !== false) { //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) NOT NULL'); + 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 (stripos($errorInfo[2], 'hash') !== false) { //v1.2 + $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN hash BINARY(16) NOT NULL'); + return $stm && $stm->execute(); + } + } catch (Exception $e) { + Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn error: ' . $e->getMessage()); + if ($hasTransaction) { + $this->bd->rollBack(); + } + } + } + } + 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(?)' : '?') + . ', ?, ?, ?, X?, ?, ?, ?, ?)'; + $this->addEntryPrepared = $this->bd->prepare($sql); + } $values = array( $valuesTmp['id'], @@ -29,55 +66,65 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $valuesTmp['content'], substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], + time(), + $valuesTmp['hash'], $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(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']); - }*/ + } 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); + private $updateEntryPrepared = null; - $eDate = $entry->date(true); - - if ($feedHistory == -2) { - $feedHistory = $conf->keep_history_default; + public function updateEntry($valuesTmp) { + if ($this->updateEntryPrepared === null) { + $sql = 'UPDATE `' . $this->prefix . 'entry` ' + . 'SET title=?, author=?, ' + . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?') + . ', link=?, date=?, lastSeen=?, hash=X?, is_read=?, tags=? ' + . 'WHERE id_feed=? AND guid=?'; + $this->updateEntryPrepared = $this->bd->prepare($sql); } - 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(); + $values = array( + substr($valuesTmp['title'], 0, 255), + substr($valuesTmp['author'], 0, 255), + $valuesTmp['content'], + substr($valuesTmp['link'], 0, 1023), + $valuesTmp['date'], + time(), + $valuesTmp['hash'], + $valuesTmp['is_read'] ? 1 : 0, + substr($valuesTmp['tags'], 0, 1023), + $valuesTmp['id_feed'], + substr($valuesTmp['guid'], 0, 760), + ); - return $this->addEntry($values); + if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) { + return $this->bd->lastInsertId(); + } else { + $info = $this->updateEntryPrepared == null ? array(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; } - - // We don't return Entry object to avoid a research in DB - return -1; } /** @@ -94,6 +141,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 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 +346,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { * * 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 +360,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $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 +374,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $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 +388,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 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 +397,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $stm = $this->bd->prepare($sql); $values = array( - $feed_id, - $id + $id_feed, + $guid, ); $stm->execute($values); @@ -519,12 +569,52 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 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(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(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/Feed.php b/app/Models/Feed.php index 5ce03be5d..27c83ffd5 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -255,6 +255,7 @@ class FreshRSS_Feed extends Minz_Model { $feed->__destruct(); //http://simplepie.org/wiki/faq/i_m_getting_memory_leaks unset($feed); + //TODO: Return a different information in case of cache/no-cache, and give access to the GUIDs in case of cache } } } diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index cf0159199..afdd821b2 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` ( `name` varchar(255) NOT NULL, `website` varchar(255) CHARACTER SET latin1, `description` text, - `lastUpdate` int(11) DEFAULT 0, + `lastUpdate` int(11) DEFAULT 0, -- Until year 2038 `priority` tinyint(2) NOT NULL DEFAULT 10, `pathEntries` varchar(511) DEFAULT NULL, `httpAuth` varchar(511) DEFAULT NULL, @@ -40,7 +40,9 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` ( `author` varchar(255), `content_bin` blob, -- v0.7 `link` varchar(1023) CHARACTER SET latin1 NOT NULL, - `date` int(11), + `date` int(11), -- Until year 2038 + `lastSeen` INT(11) NOT NULL, -- v1.2, Until year 2038 + `hash` BINARY(16), -- v1.2 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, `id_feed` SMALLINT, -- v0.7 @@ -50,6 +52,7 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` ( UNIQUE KEY (`id_feed`,`guid`), -- v0.7 INDEX (`is_favorite`), -- v0.7 INDEX (`is_read`) -- v0.7 + INDEX entry_lastSeen_index (`lastSeen`) -- v1.2 ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB; diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index 30bca2810..7517ead45 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -14,7 +14,7 @@ $SQL_CREATE_TABLES = array( `name` varchar(255) NOT NULL, `website` varchar(255), `description` text, - `lastUpdate` int(11) DEFAULT 0, + `lastUpdate` int(11) DEFAULT 0, -- Until year 2038 `priority` tinyint(2) NOT NULL DEFAULT 10, `pathEntries` varchar(511) DEFAULT NULL, `httpAuth` varchar(511) DEFAULT NULL, @@ -38,7 +38,9 @@ $SQL_CREATE_TABLES = array( `author` varchar(255), `content` text, `link` varchar(1023) NOT NULL, - `date` int(11), + `date` int(11), -- Until year 2038 + `lastSeen` INT(11) NOT NULL, -- v1.2, Until year 2038 + `hash` BINARY(16), -- v1.2 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, `id_feed` SMALLINT, @@ -50,6 +52,7 @@ $SQL_CREATE_TABLES = array( 'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `%1$sentry`(`is_favorite`);', 'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);', +'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);', //v1.2 'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");', ); diff --git a/lib/lib_rss.php b/lib/lib_rss.php index e5fe73041..c6bdfde0e 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -38,7 +38,7 @@ function classAutoloader($class) { include(APP_PATH . '/Models/' . $components[1] . '.php'); return; case 3: //Controllers, Exceptions - @include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php'); + include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php'); return; } } elseif (strpos($class, 'Minz') === 0) { -- cgit v1.2.3 From d229216cccbd746b46630a44fa508ef0367ea1a1 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 22 Apr 2015 00:24:22 -0400 Subject: Split the search into values Before, the search was a single value. Now it is splited in chuncks when separated by spaces. Except if they are enclosed by single quotes or double quotes. For some reasons, the unit tests are working for both single and double quotes but the search box isn't. It is working only with single quotes. We need to investigate the reason of this behavior. See #823 --- app/Models/EntryDAO.php | 8 +++-- app/Models/Search.php | 32 +++++++++++++++++-- tests/app/Models/SearchTest.php | 68 +++++++++++++++++++++++++---------------- 3 files changed, 77 insertions(+), 31 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 9736d5cd3..5bdc216bc 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -478,11 +478,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } if ($filter->getSearch()) { - $search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? '; - $values[] = "%{$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 ' : '') diff --git a/app/Models/Search.php b/app/Models/Search.php index 84688be2e..575a9a2cb 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -34,9 +34,9 @@ class FreshRSS_Search { $input = $this->parsePubdateSearch($input); $input = $this->parseDateSearch($input); $input = $this->parseTagsSeach($input); - $this->search = $this->cleanSearch($input); + $this->parseSearch($input); } - + public function __toString() { return $this->getRawInput(); } @@ -187,6 +187,34 @@ class FreshRSS_Search { 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[\'"])(?P.*)(?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 * diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php index 9e3ca6765..73ff56cc6 100644 --- a/tests/app/Models/SearchTest.php +++ b/tests/app/Models/SearchTest.php @@ -51,20 +51,21 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideIntitleSearch() { return array( array('intitle:word1', 'word1', null), - array('intitle:word1 word2', 'word1', 'word2'), + array('intitle:word1 word2', 'word1', array('word2')), array('intitle:"word1 word2"', 'word1 word2', null), array("intitle:'word1 word2'", 'word1 word2', null), - array('word1 intitle:word2', 'word2', 'word1'), - array('word1 intitle:word2 word3', 'word2', 'word1 word3'), - array('word1 intitle:"word2 word3"', 'word2 word3', 'word1'), - array("word1 intitle:'word2 word3'", 'word2 word3', 'word1'), - array('intitle:word1 intitle:word2', 'word1', 'intitle:word2'), - array('intitle: word1 word2', null, 'word1 word2'), + array('word1 intitle:word2', 'word2', array('word1')), + array('word1 intitle:word2 word3', 'word2', array('word1', 'word3')), + array('word1 intitle:"word2 word3"', 'word2 word3', array('word1')), + array("word1 intitle:'word2 word3'", 'word2 word3', array('word1')), + array('intitle:word1 intitle:word2', 'word1', array('intitle:word2')), + array('intitle: word1 word2', null, array('word1', 'word2')), array('intitle:123', '123', null), - array('intitle:"word1 word2" word3"', 'word1 word2', 'word3"'), - array("intitle:'word1 word2' word3'", 'word1 word2', "word3'"), + array('intitle:"word1 word2" word3"', 'word1 word2', array('word3"')), + array("intitle:'word1 word2' word3'", 'word1 word2', array("word3'")), array('intitle:"word1 word2\' word3"', "word1 word2' word3", null), array("intitle:'word1 word2\" word3'", 'word1 word2" word3', null), + array("intitle:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), ); } @@ -86,20 +87,21 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideAuthorSearch() { return array( array('author:word1', 'word1', null), - array('author:word1 word2', 'word1', 'word2'), + array('author:word1 word2', 'word1', array('word2')), array('author:"word1 word2"', 'word1 word2', null), array("author:'word1 word2'", 'word1 word2', null), - array('word1 author:word2', 'word2', 'word1'), - array('word1 author:word2 word3', 'word2', 'word1 word3'), - array('word1 author:"word2 word3"', 'word2 word3', 'word1'), - array("word1 author:'word2 word3'", 'word2 word3', 'word1'), - array('author:word1 author:word2', 'word1', 'author:word2'), - array('author: word1 word2', null, 'word1 word2'), + array('word1 author:word2', 'word2', array('word1')), + array('word1 author:word2 word3', 'word2', array('word1', 'word3')), + array('word1 author:"word2 word3"', 'word2 word3', array('word1')), + array("word1 author:'word2 word3'", 'word2 word3', array('word1')), + array('author:word1 author:word2', 'word1', array('author:word2')), + array('author: word1 word2', null, array('word1', 'word2')), array('author:123', '123', null), - array('author:"word1 word2" word3"', 'word1 word2', 'word3"'), - array("author:'word1 word2' word3'", 'word1 word2', "word3'"), + array('author:"word1 word2" word3"', 'word1 word2', array('word3"')), + array("author:'word1 word2' word3'", 'word1 word2', array("word3'")), array('author:"word1 word2\' word3"', "word1 word2' word3", null), array("author:'word1 word2\" word3'", 'word1 word2" word3', null), + array("author:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), ); } @@ -121,10 +123,11 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideInurlSearch() { return array( array('inurl:word1', 'word1', null), - array('inurl: word1', null, 'word1'), + array('inurl: word1', null, array('word1')), array('inurl:123', '123', null), - array('inurl:word1 word2', 'word1', 'word2'), - array('inurl:"word1 word2"', '"word1', 'word2"'), + array('inurl:word1 word2', 'word1', array('word2')), + array('inurl:"word1 word2"', '"word1', array('word2"')), + array("inurl:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), ); } @@ -198,11 +201,12 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideTagsSearch() { return array( array('#word1', array('word1'), null), - array('# word1', null, '# word1'), + array('# word1', null, array('#', 'word1')), array('#123', array('123'), null), - array('#word1 word2', array('word1'), 'word2'), - array('#"word1 word2"', array('"word1'), 'word2"'), + array('#word1 word2', array('word1'), array('word2')), + array('#"word1 word2"', array('"word1'), array('word2"')), array('#word1 #word2', array('word1', 'word2'), null), + array("#word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')), ); } @@ -257,7 +261,7 @@ class SearchTest extends \PHPUnit_Framework_TestCase { '1172725200', '1210564799', array('word4', 'word5'), - 'word6', + array('word6'), ), array( 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 word7 date:2007-03-01/2008-05-11', @@ -269,7 +273,19 @@ class SearchTest extends \PHPUnit_Framework_TestCase { '1172725200', '1210564799', array('word4', 'word5'), - 'word6 word7', + array('word6', 'word7'), + ), + array( + 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 "word7 word8" date:2007-03-01/2008-05-11', + 'word1', + '1172725200', + '1210564799', + 'word2', + 'word3', + '1172725200', + '1210564799', + array('word4', 'word5'), + array('word7 word8', 'word6'), ), ); } -- cgit v1.2.3 From 7f7de31c1dcb6599be5c5713f36b4bde1d03d47a Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 9 May 2015 13:07:54 +0200 Subject: SQL: update request for updated articles https://github.com/FreshRSS/FreshRSS/issues/798 --- app/Controllers/feedController.php | 2 +- app/Models/Entry.php | 4 ++-- app/Models/EntryDAO.php | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 08a0257a2..59c9174fb 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -343,7 +343,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } else { //This entry already exists but has been updated Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() . ', old hash ' . $existingHash . ', new hash ' . $entry->hash()); - $entry->_isRead($is_read); //Reset is_read + $entry->_isRead(null); //Change is_read according to policy. //TODO: Implement option if (!$entryDAO->hasTransaction()) { $entryDAO->beginTransaction(); } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 6931c9f25..a562a963a 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -15,7 +15,7 @@ class FreshRSS_Entry extends Minz_Model { private $link; private $date; private $hash = null; - private $is_read; + private $is_read; //Nullable boolean private $is_favorite; private $feed; private $tags; @@ -125,7 +125,7 @@ class FreshRSS_Entry extends Minz_Model { $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; diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 5b4b85547..543b61573 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -91,11 +91,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { private $updateEntryPrepared = null; public function updateEntry($valuesTmp) { + if (!isset($valuesTmp['is_read'])) { + $valuesTmp['is_read'] = null; + } + if ($this->updateEntryPrepared === null) { $sql = 'UPDATE `' . $this->prefix . 'entry` ' . 'SET title=?, author=?, ' . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?') - . ', link=?, date=?, lastSeen=?, hash=X?, is_read=?, tags=? ' + . ', link=?, date=?, lastSeen=?, hash=X?, ' + . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ') + . 'tags=? ' . 'WHERE id_feed=? AND guid=?'; $this->updateEntryPrepared = $this->bd->prepare($sql); } @@ -108,11 +114,15 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $valuesTmp['date'], time(), $valuesTmp['hash'], - $valuesTmp['is_read'] ? 1 : 0, + ); + 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), - ); + )); if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) { return $this->bd->lastInsertId(); -- cgit v1.2.3 From 0745252b68f6f9b7c91ea437893b5f33b7a224c3 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 20:31:03 +0200 Subject: Hexadecimal literals do not work with SQLite/PDO X'09AF' hexadecimal literals do not work with SQLite/PDO. Replaced by PHP hex2bin(). https://github.com/FreshRSS/FreshRSS/commit/711530a512b370d79b079205ce1f8376174f7f03 --- app/Models/EntryDAO.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index ebaeb3868..172eac897 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -54,7 +54,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) ' . 'VALUES(?, ?, ?, ?, ' . ($this->isCompressed() ? 'COMPRESS(?)' : '?') - . ', ?, ?, ?, X?, ?, ?, ?, ?)'; + . ', ?, ?, ?, ?, ?, ?, ?, ?)'; $this->addEntryPrepared = $this->bd->prepare($sql); } @@ -67,7 +67,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], time(), - $valuesTmp['hash'], + 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'], @@ -77,7 +77,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($this->addEntryPrepared && $this->addEntryPrepared->execute($values)) { return $this->bd->lastInsertId(); } else { - $info = $this->addEntryPrepared == null ? array(2 => 'syntax error') : $this->addEntryPrepared->errorInfo(); + $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 @@ -99,7 +99,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $sql = 'UPDATE `' . $this->prefix . 'entry` ' . 'SET title=?, author=?, ' . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?') - . ', link=?, date=?, lastSeen=?, hash=X?, ' + . ', link=?, date=?, lastSeen=?, hash=?, ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ') . 'tags=? ' . 'WHERE id_feed=? AND guid=?'; @@ -113,7 +113,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], time(), - $valuesTmp['hash'], + hex2bin($valuesTmp['hash']), ); if ($valuesTmp['is_read'] !== null) { $values[] = $valuesTmp['is_read'] ? 1 : 0; @@ -127,7 +127,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) { return $this->bd->lastInsertId(); } else { - $info = $this->updateEntryPrepared == null ? array(2 => 'syntax error') : $this->updateEntryPrepared->errorInfo(); + $info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo(); if ($this->autoAddColumn($info)) { return $this->updateEntry($valuesTmp); } @@ -598,7 +598,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $result; } else { - $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); if ($this->autoAddColumn($info)) { return $this->listHashForFeedGuids($id_feed, $guids); } @@ -619,7 +619,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); if ($this->autoAddColumn($info)) { return $this->updateLastSeen($id_feed, $guids); } -- cgit v1.2.3 From 217c191a1ba3ac03b847d261a32e19975380fcad Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Mon, 11 May 2015 22:42:41 +0200 Subject: More SQLite compatibility Additional changes to add compatibility with SQLite for the new hash/lastSeen mode of updating articles. --- app/Models/EntryDAO.php | 71 ++++++++++++++++++++++++------------------ app/Models/EntryDAOSQLite.php | 15 +++++++++ app/Models/FeedDAO.php | 11 ++++--- app/SQL/install.sql.mysql.php | 2 +- app/SQL/install.sql.sqlite.php | 2 +- app/install.php | 2 ++ lib/Minz/ModelPdo.php | 5 +++ 7 files changed, 70 insertions(+), 38 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 172eac897..eae9683ad 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -6,38 +6,48 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return parent::$sharedDbType !== 'sqlite'; } - protected function autoAddColumn($errorInfo) { - if (isset($errorInfo[0])) { - if ($errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR - $hasTransaction = false; - try { - $stm = null; - if (stripos($errorInfo[2], 'lastSeen') !== false) { //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) NOT NULL'); - 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; - } - } + 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->rollBack(); + $this->bd->commit(); } - } elseif (stripos($errorInfo[2], 'hash') !== false) { //v1.2 - $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN hash BINARY(16) NOT NULL'); - return $stm && $stm->execute(); + return true; } - } catch (Exception $e) { - Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn error: ' . $e->getMessage()); - if ($hasTransaction) { - $this->bd->rollBack(); + } + 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); } } } @@ -82,7 +92,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 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']); + . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']. ' ' . $this->addEntryPrepared); } return false; } @@ -597,7 +607,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } return $result; } else { - $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); if ($this->autoAddColumn($info)) { return $this->listHashForFeedGuids($id_feed, $guids); 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/FeedDAO.php b/app/Models/FeedDAO.php index 76025ff53..475d39286 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -330,11 +330,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . '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/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index afdd821b2..9c6af405d 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` ( `content_bin` blob, -- v0.7 `link` varchar(1023) CHARACTER SET latin1 NOT NULL, `date` int(11), -- Until year 2038 - `lastSeen` INT(11) NOT NULL, -- v1.2, Until year 2038 + `lastSeen` INT(11) DEFAULT 0, -- v1.2, Until year 2038 `hash` BINARY(16), -- v1.2 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index 7517ead45..77e8e094c 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -39,7 +39,7 @@ $SQL_CREATE_TABLES = array( `content` text, `link` varchar(1023) NOT NULL, `date` int(11), -- Until year 2038 - `lastSeen` INT(11) NOT NULL, -- v1.2, Until year 2038 + `lastSeen` INT(11) DEFAULT 0, -- v1.2, Until year 2038 `hash` BINARY(16), -- v1.2 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, diff --git a/app/install.php b/app/install.php index 177173fdb..86afb9318 100644 --- a/app/install.php +++ b/app/install.php @@ -168,8 +168,10 @@ function saveStep3() { $_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] .(empty($_SESSION['default_user']) ? '' :($_SESSION['default_user'] . '_')); } + //TODO: load `config.default.php` as default $config_array = array( 'environment' => 'production', + 'simplepie_syslog_enabled' => true, 'salt' => $_SESSION['salt'], 'title' => $_SESSION['title'], 'default_user' => $_SESSION['default_user'], diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index ac7a1bed7..3e8ec1f43 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -134,4 +134,9 @@ class MinzPDO extends PDO { MinzPDO::check($statement); return parent::exec($statement); } + + public function query($statement) { + MinzPDO::check($statement); + return parent::query($statement); + } } -- cgit v1.2.3 From 9d55ee5ae9e41fe460ff82b4d51bf1673fb1b836 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 24 May 2015 01:49:13 +0200 Subject: Bug EntryDAO filter https://github.com/FreshRSS/FreshRSS/issues/850 --- app/Models/EntryDAO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index eae9683ad..f939a0fb3 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -511,7 +511,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $where .= 'AND e1.id >= ' . $date_min . '000000 '; } $search = ''; - if ($filter !== null) { + if ($filter) { if ($filter->getIntitle()) { $search .= 'AND e1.title LIKE ? '; $values[] = "%{$filter->getIntitle()}%"; -- cgit v1.2.3 From 96ba71e618468f7d28a04c4ebc7c46dd912ccd75 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 31 May 2015 20:22:27 +0200 Subject: MySQL create table bug https://github.com/FreshRSS/FreshRSS/issues/845 And updated version comments to 1.1.1 --- app/Models/EntryDAO.php | 2 +- app/SQL/install.sql.mysql.php | 8 ++++---- app/SQL/install.sql.sqlite.php | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index f939a0fb3..bd575989d 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -92,7 +92,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 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']. ' ' . $this->addEntryPrepared); + . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']); } return false; } diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index 9c6af405d..c5787d25b 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -41,8 +41,8 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` ( `content_bin` blob, -- v0.7 `link` varchar(1023) CHARACTER SET latin1 NOT NULL, `date` int(11), -- Until year 2038 - `lastSeen` INT(11) DEFAULT 0, -- v1.2, Until year 2038 - `hash` BINARY(16), -- v1.2 + `lastSeen` INT(11) DEFAULT 0, -- v1.1.1, Until year 2038 + `hash` BINARY(16), -- v1.1.1 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, `id_feed` SMALLINT, -- v0.7 @@ -51,8 +51,8 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` ( FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY (`id_feed`,`guid`), -- v0.7 INDEX (`is_favorite`), -- v0.7 - INDEX (`is_read`) -- v0.7 - INDEX entry_lastSeen_index (`lastSeen`) -- v1.2 + INDEX (`is_read`), -- v0.7 + INDEX `entry_lastSeen_index` (`lastSeen`) -- v1.1.1 ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB; diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index 77e8e094c..71bad7311 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -39,8 +39,8 @@ $SQL_CREATE_TABLES = array( `content` text, `link` varchar(1023) NOT NULL, `date` int(11), -- Until year 2038 - `lastSeen` INT(11) DEFAULT 0, -- v1.2, Until year 2038 - `hash` BINARY(16), -- v1.2 + `lastSeen` INT(11) DEFAULT 0, -- v1.1.1, Until year 2038 + `hash` BINARY(16), -- v1.1.1 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, `id_feed` SMALLINT, @@ -52,7 +52,7 @@ $SQL_CREATE_TABLES = array( 'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `%1$sentry`(`is_favorite`);', 'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);', -'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);', //v1.2 +'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);', //v1.1.1 'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");', ); -- cgit v1.2.3 From 384a146883548ba0274f8cbee0c2e67dc053f70e Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 31 May 2015 20:48:18 +0200 Subject: Minor comment 1.1.1 https://github.com/FreshRSS/FreshRSS/issues/845 --- app/Models/EntryDAO.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index bd575989d..9ddcfcfb3 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -11,7 +11,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $hasTransaction = false; try { $stm = null; - if ($name === 'lastSeen') { //v1.2 + if ($name === 'lastSeen') { //v1.1.1 if (!$this->bd->inTransaction()) { $this->bd->beginTransaction(); $hasTransaction = true; @@ -29,7 +29,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($hasTransaction) { $this->bd->rollBack(); } - } elseif ($name === 'hash') { //v1.2 + } elseif ($name === 'hash') { //v1.1.1 $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN hash BINARY(16)'); return $stm && $stm->execute(); } -- cgit v1.2.3 From 6b7d94626656674b60d6f970bd4ada46383dde1e Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 10 Jul 2015 21:40:28 +0200 Subject: Avoid hex2bin for PHP 5.3 https://github.com/FreshRSS/FreshRSS/issues/894 And use native hexadecimal function when available (MySQL) to avoid having binary data in the SQL logs. --- app/Models/EntryDAO.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 9ddcfcfb3..f74055835 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -6,6 +6,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return parent::$sharedDbType !== 'sqlite'; } + public function hasNativeHex() { + return parent::$sharedDbType !== 'sqlite'; + } + protected function addColumn($name) { Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn: ' . $name); $hasTransaction = false; @@ -64,7 +68,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) ' . 'VALUES(?, ?, ?, ?, ' . ($this->isCompressed() ? 'COMPRESS(?)' : '?') - . ', ?, ?, ?, ?, ?, ?, ?, ?)'; + . ', ?, ?, ?, ' + . ($this->hasNativeHex() ? 'X?' : '?') + . ', ?, ?, ?, ?)'; $this->addEntryPrepared = $this->bd->prepare($sql); } @@ -77,7 +83,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], time(), - hex2bin($valuesTmp['hash']), // X'09AF' hexadecimal literals do not work with SQLite/PDO + $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'], @@ -109,8 +115,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $sql = 'UPDATE `' . $this->prefix . 'entry` ' . 'SET title=?, author=?, ' . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?') - . ', link=?, date=?, lastSeen=?, hash=?, ' - . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ') + . ', 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); @@ -123,7 +130,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], time(), - hex2bin($valuesTmp['hash']), + $this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']), ); if ($valuesTmp['is_read'] !== null) { $values[] = $valuesTmp['is_read'] ? 1 : 0; -- cgit v1.2.3