From 4bb8548eccf22b40c25352fe27c66f1f8039ebcd Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sun, 8 Feb 2015 20:51:41 -0500 Subject: Add a way to parse search string to extract keywords This feature is not in use at the moment, but it will be handy to reorganize the query building process. It allows to have more than one keyword in the search box. Full tests are available as well. It probably needs a refactoring later, but I think this is the first step to make the application full object oriented and testable. --- app/Models/Context.php | 149 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) (limited to 'app/Models') diff --git a/app/Models/Context.php b/app/Models/Context.php index 1c770c756..645639907 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -301,4 +301,153 @@ class FreshRSS_Context { } return false; } + + /** + * Parse search string to extract the different keywords. + * + * @return array + */ + public function parseSearch() { + $search = self::$search; + $intitle = $this->parseIntitleSearch($search); + $author = $this->parseAuthorSearch($intitle['string']); + $inurl = $this->parseInurlSearch($author['string']); + $pubdate = $this->parsePubdateSearch($inurl['string']); + $date = $this->parseDateSearch($pubdate['string']); + + $remaining = array(); + $remaining_search = trim($date['string']); + if (strcmp($remaining_search, '') != 0) { + $remaining['search'] = $remaining_search; + } + + return array_merge($intitle['search'], $author['search'], $inurl['search'], $date['search'], $pubdate['search'], $remaining); + } + + /** + * Parse the search string to find intitle keyword and the search related + * to it. + * The search is the first word following the keyword. + * It returns an array containing the matched string and the search. + * + * @param string $search + * @return array + */ + private function parseIntitleSearch($search) { + if (preg_match('/intitle:(?P[\'"])(?P.*)(?P=delim)/U', $search, $matches)) { + return array( + 'string' => str_replace($matches[0], '', $search), + 'search' => array('intitle' => $matches['search']), + ); + } + if (preg_match('/intitle:(?P\w*)/', $search, $matches)) { + return array( + 'string' => str_replace($matches[0], '', $search), + 'search' => array('intitle' => $matches['search']), + ); + } + return array( + 'string' => $search, + 'search' => array(), + ); + } + + /** + * Parse the search string to find author keyword and the search related + * to it. + * The search is the first word following the keyword except when using + * a delimiter. Supported delimiters are single quote (') and double + * quotes ("). + * It returns an array containing the matched string and the search. + * + * @param string $search + * @return array + */ + private function parseAuthorSearch($search) { + if (preg_match('/author:(?P[\'"])(?P.*)(?P=delim)/U', $search, $matches)) { + return array( + 'string' => str_replace($matches[0], '', $search), + 'search' => array('author' => $matches['search']), + ); + } + if (preg_match('/author:(?P\w*)/', $search, $matches)) { + return array( + 'string' => str_replace($matches[0], '', $search), + 'search' => array('author' => $matches['search']), + ); + } + return array( + 'string' => $search, + 'search' => array(), + ); + } + + /** + * Parse the search string to find inurl keyword and the search related + * to it. + * The search is the first word following the keyword except. + * It returns an array containing the matched string and the search. + * + * @param string $search + * @return array + */ + private function parseInurlSearch($search) { + if (preg_match('/inurl:(?P[^\s]*)/', $search, $matches)) { + return array( + 'string' => str_replace($matches[0], '', $search), + 'search' => array('inurl' => $matches['search']), + ); + } + return array( + 'string' => $search, + 'search' => array(), + ); + } + + /** + * Parse the search string to find date keyword and the search related + * to it. + * The search is the first word following the keyword. + * It returns an array containing the matched string and the search. + * + * @param string $search + * @return array + */ + private function parseDateSearch($search) { + if (preg_match('/date:(?P[^\s]*)/', $search, $matches)) { + list($min_date, $max_date) = parseDateInterval($matches['search']); + return array( + 'string' => str_replace($matches[0], '', $search), + 'search' => array('min_date' => $min_date, 'max_date' => $max_date), + ); + } + return array( + 'string' => $search, + 'search' => array(), + ); + } + + /** + * Parse the search string to find pubdate keyword and the search related + * to it. + * The search is the first word following the keyword. + * It returns an array containing the matched string and the search. + * + * @param string $search + * @return array + */ + private function parsePubdateSearch($search) { + if (preg_match('/pubdate:(?P[^\s]*)/', $search, $matches)) { + list($min_date, $max_date) = parseDateInterval($matches['search']); + return array( + 'string' => str_replace($matches[0], '', $search), + 'search' => array('min_pubdate' => $min_date, 'max_pubdate' => $max_date), + ); + } + return array( + 'string' => $search, + 'search' => array(), + ); + } + } -- cgit v1.2.3 From f30ded2f863ed4a33ae8194d6cf00eaa877c9b6a Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 11 Feb 2015 20:41:00 -0500 Subject: Extract the search code from the context I figured that the code for the search could be extracted from the context to have separation of concern. It supports multiple keywords. It suports also multiple tag keywords. --- app/Models/Context.php | 148 --------------------- app/Models/Search.php | 189 +++++++++++++++++++++++++++ tests/app/Models/ContextTest.php | 236 --------------------------------- tests/app/Models/SearchTest.php | 276 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 465 insertions(+), 384 deletions(-) create mode 100644 app/Models/Search.php create mode 100644 tests/app/Models/SearchTest.php (limited to 'app/Models') diff --git a/app/Models/Context.php b/app/Models/Context.php index 645639907..f00bb1e97 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -302,152 +302,4 @@ class FreshRSS_Context { return false; } - /** - * Parse search string to extract the different keywords. - * - * @return array - */ - public function parseSearch() { - $search = self::$search; - $intitle = $this->parseIntitleSearch($search); - $author = $this->parseAuthorSearch($intitle['string']); - $inurl = $this->parseInurlSearch($author['string']); - $pubdate = $this->parsePubdateSearch($inurl['string']); - $date = $this->parseDateSearch($pubdate['string']); - - $remaining = array(); - $remaining_search = trim($date['string']); - if (strcmp($remaining_search, '') != 0) { - $remaining['search'] = $remaining_search; - } - - return array_merge($intitle['search'], $author['search'], $inurl['search'], $date['search'], $pubdate['search'], $remaining); - } - - /** - * Parse the search string to find intitle keyword and the search related - * to it. - * The search is the first word following the keyword. - * It returns an array containing the matched string and the search. - * - * @param string $search - * @return array - */ - private function parseIntitleSearch($search) { - if (preg_match('/intitle:(?P[\'"])(?P.*)(?P=delim)/U', $search, $matches)) { - return array( - 'string' => str_replace($matches[0], '', $search), - 'search' => array('intitle' => $matches['search']), - ); - } - if (preg_match('/intitle:(?P\w*)/', $search, $matches)) { - return array( - 'string' => str_replace($matches[0], '', $search), - 'search' => array('intitle' => $matches['search']), - ); - } - return array( - 'string' => $search, - 'search' => array(), - ); - } - - /** - * Parse the search string to find author keyword and the search related - * to it. - * The search is the first word following the keyword except when using - * a delimiter. Supported delimiters are single quote (') and double - * quotes ("). - * It returns an array containing the matched string and the search. - * - * @param string $search - * @return array - */ - private function parseAuthorSearch($search) { - if (preg_match('/author:(?P[\'"])(?P.*)(?P=delim)/U', $search, $matches)) { - return array( - 'string' => str_replace($matches[0], '', $search), - 'search' => array('author' => $matches['search']), - ); - } - if (preg_match('/author:(?P\w*)/', $search, $matches)) { - return array( - 'string' => str_replace($matches[0], '', $search), - 'search' => array('author' => $matches['search']), - ); - } - return array( - 'string' => $search, - 'search' => array(), - ); - } - - /** - * Parse the search string to find inurl keyword and the search related - * to it. - * The search is the first word following the keyword except. - * It returns an array containing the matched string and the search. - * - * @param string $search - * @return array - */ - private function parseInurlSearch($search) { - if (preg_match('/inurl:(?P[^\s]*)/', $search, $matches)) { - return array( - 'string' => str_replace($matches[0], '', $search), - 'search' => array('inurl' => $matches['search']), - ); - } - return array( - 'string' => $search, - 'search' => array(), - ); - } - - /** - * Parse the search string to find date keyword and the search related - * to it. - * The search is the first word following the keyword. - * It returns an array containing the matched string and the search. - * - * @param string $search - * @return array - */ - private function parseDateSearch($search) { - if (preg_match('/date:(?P[^\s]*)/', $search, $matches)) { - list($min_date, $max_date) = parseDateInterval($matches['search']); - return array( - 'string' => str_replace($matches[0], '', $search), - 'search' => array('min_date' => $min_date, 'max_date' => $max_date), - ); - } - return array( - 'string' => $search, - 'search' => array(), - ); - } - - /** - * Parse the search string to find pubdate keyword and the search related - * to it. - * The search is the first word following the keyword. - * It returns an array containing the matched string and the search. - * - * @param string $search - * @return array - */ - private function parsePubdateSearch($search) { - if (preg_match('/pubdate:(?P[^\s]*)/', $search, $matches)) { - list($min_date, $max_date) = parseDateInterval($matches['search']); - return array( - 'string' => str_replace($matches[0], '', $search), - 'search' => array('min_pubdate' => $min_date, 'max_pubdate' => $max_date), - ); - } - return array( - 'string' => $search, - 'search' => array(), - ); - } - } diff --git a/app/Models/Search.php b/app/Models/Search.php new file mode 100644 index 000000000..0475bc685 --- /dev/null +++ b/app/Models/Search.php @@ -0,0 +1,189 @@ +raw_input = $input; + $input = $this->parseIntitleSearch($input); + $input = $this->parseAuthorSearch($input); + $input = $this->parseInurlSearch($input); + $input = $this->parsePubdateSearch($input); + $input = $this->parseDateSearch($input); + $input = $this->parseTagsSeach($input); + $this->search = $input; + } + + public function getRawInput() { + return $this->raw_input; + } + + public function getIntitle() { + return $this->intitle; + } + + public function getMinDate() { + return $this->min_date; + } + + public function getMaxDate() { + return $this->max_date; + } + + public function getMinPubdate() { + return $this->min_pubdate; + } + + public function getMaxPubdate() { + return $this->max_pubdate; + } + + public function getInurl() { + return $this->inurl; + } + + public function getAuthor() { + return $this->author; + } + + public function getTags() { + return $this->tags; + } + + public function getSearch() { + return $this->search; + } + + /** + * Parse the search string to find intitle keyword and the search related + * to it. + * The search is the first word following the keyword. + * + * @param string $input + * @return string + */ + private function parseIntitleSearch($input) { + if (preg_match('/intitle:(?P[\'"])(?P.*)(?P=delim)/U', $input, $matches)) { + $this->intitle = $matches['search']; + $input = str_replace($matches[0], '', $input); + } else if (preg_match('/intitle:(?P\w*)/', $input, $matches)) { + $this->intitle = $matches['search']; + $input = str_replace($matches[0], '', $input); + } + return $this->cleanSearch($input); + } + + /** + * Parse the search string to find author keyword and the search related + * to it. + * The search is the first word following the keyword except when using + * a delimiter. Supported delimiters are single quote (') and double + * quotes ("). + * + * @param string $input + * @return string + */ + private function parseAuthorSearch($input) { + if (preg_match('/author:(?P[\'"])(?P.*)(?P=delim)/U', $input, $matches)) { + $this->author = $matches['search']; + $input = str_replace($matches[0], '', $input); + } else if (preg_match('/author:(?P\w*)/', $input, $matches)) { + $this->author = $matches['search']; + $input = str_replace($matches[0], '', $input); + } + return $this->cleanSearch($input); + } + + /** + * Parse the search string to find inurl keyword and the search related + * to it. + * The search is the first word following the keyword except. + * + * @param string $input + * @return string + */ + private function parseInurlSearch($input) { + if (preg_match('/inurl:(?P[^\s]*)/', $input, $matches)) { + $this->inurl = $matches['search']; + $input = str_replace($matches[0], '', $input); + } + return $this->cleanSearch($input); + } + + /** + * Parse the search string to find date keyword and the search related + * to it. + * The search is the first word following the keyword. + * + * @param string $input + * @return string + */ + private function parseDateSearch($input) { + if (preg_match('/date:(?P[^\s]*)/', $input, $matches)) { + list($this->min_date, $this->max_date) = parseDateInterval($matches['search']); + $input = str_replace($matches[0], '', $input); + } + return $this->cleanSearch($input); + } + + /** + * Parse the search string to find pubdate keyword and the search related + * to it. + * The search is the first word following the keyword. + * + * @param string $input + * @return string + */ + private function parsePubdateSearch($input) { + if (preg_match('/pubdate:(?P[^\s]*)/', $input, $matches)) { + list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($matches['search']); + $input = str_replace($matches[0], '', $input); + } + return $this->cleanSearch($input); + } + + /** + * Parse the search string to find tags keyword (# followed by a word) + * and the search related to it. + * The search is the first word following the #. + * + * @param string $input + * @return string + */ + private function parseTagsSeach($input) { + if (preg_match_all('/#(?P[^\s]+)/', $input, $matches)) { + $this->tags = $matches['search']; + $input = str_replace($matches[0], '', $input); + } + return $this->cleanSearch($input); + } + + private function cleanSearch($input) { + $input = preg_replace('/\s+/', ' ', $input); + return trim($input); + } + +} diff --git a/tests/app/Models/ContextTest.php b/tests/app/Models/ContextTest.php index c5da6f667..4dc8b7757 100644 --- a/tests/app/Models/ContextTest.php +++ b/tests/app/Models/ContextTest.php @@ -1,241 +1,5 @@ context = new FreshRSS_Context(); - } - - public function testParseSearch_whenEmpty_returnsEmptyArray() { - $this->assertCount(0, $this->context->parseSearch()); - } - - /** - * @dataProvider provideMultipleKeywordSearch - * @param string $search - * @param string $expected_values - */ - public function testParseSearch_whenMultipleKeywords_returnArrayWithMultipleValues($search, $expected_values) { - FreshRSS_Context::$search = $search; - $parsed_search = $this->context->parseSearch(); - $this->assertEquals($expected_values, $parsed_search); - } - - /** - * @return array - */ - public function provideMultipleKeywordSearch() { - return array( - array( - 'intitle:word1 author:word2', - array( - 'intitle' => 'word1', - 'author' => 'word2', - ), - ), - array( - 'author:word2 intitle:word1', - array( - 'intitle' => 'word1', - 'author' => 'word2', - ), - ), - array( - 'author:word1 inurl:word2', - array( - 'author' => 'word1', - 'inurl' => 'word2', - ), - ), - array( - 'inurl:word2 author:word1', - array( - 'author' => 'word1', - 'inurl' => 'word2', - ), - ), - array( - 'date:2008-01-01/2008-02-01 pubdate:2007-01-01/2007-02-01', - array( - 'min_date' => '1199163600', - 'max_date' => '1201928399', - 'min_pubdate' => '1167627600', - 'max_pubdate' => '1170392399', - ), - ), - array( - 'pubdate:2007-01-01/2007-02-01 date:2008-01-01/2008-02-01', - array( - 'min_date' => '1199163600', - 'max_date' => '1201928399', - 'min_pubdate' => '1167627600', - 'max_pubdate' => '1170392399', - ), - ), - array( - 'inurl:word1 author:word2 intitle:word3 pubdate:2007-01-01/2007-02-01 date:2008-01-01/2008-02-01 hello world', - array( - 'inurl' => 'word1', - 'author' => 'word2', - 'intitle' => 'word3', - 'min_date' => '1199163600', - 'max_date' => '1201928399', - 'min_pubdate' => '1167627600', - 'max_pubdate' => '1170392399', - 'search' => 'hello world', - ), - ), - ); - } - - /** - * @dataProvider provideIntitleSearch - * @param string $search - * @param string $expected_value - */ - public function testParseSearch_whenIntitleKeyword_returnArrayWithIntitleValue($search, $expected_value) { - FreshRSS_Context::$search = $search; - $parsed_search = $this->context->parseSearch(); - $this->assertEquals($expected_value, $parsed_search['intitle']); - } - - /** - * @return array - */ - public function provideIntitleSearch() { - return array( - array('intitle:word1', 'word1'), - array('intitle:word1 word2', 'word1'), - array('intitle:"word1 word2"', 'word1 word2'), - array("intitle:'word1 word2'", 'word1 word2'), - array('word1 intitle:word2', 'word2'), - array('word1 intitle:word2 word3', 'word2'), - array('word1 intitle:"word2 word3"', 'word2 word3'), - array("word1 intitle:'word2 word3'", 'word2 word3'), - array('intitle:word1 intitle:word2', 'word1'), - array('intitle: word1 word2', ''), - array('intitle:123', '123'), - array('intitle:"word1 word2" word3"', 'word1 word2'), - array("intitle:'word1 word2' word3'", 'word1 word2'), - array('intitle:"word1 word2\' word3"', "word1 word2' word3"), - array("intitle:'word1 word2\" word3'", 'word1 word2" word3'), - ); - } - - /** - * @dataProvider provideAuthorSearch - * @param string $search - * @param string $expected_value - */ - public function testParseSearch_whenAuthorKeyword_returnArrayWithAuthorValue($search, $expected_value) { - FreshRSS_Context::$search = $search; - $parsed_search = $this->context->parseSearch(); - $this->assertEquals($expected_value, $parsed_search['author']); - } - - /** - * @return array - */ - public function provideAuthorSearch() { - return array( - array('author:word1', 'word1'), - array('author:word1 word2', 'word1'), - array('author:"word1 word2"', 'word1 word2'), - array("author:'word1 word2'", 'word1 word2'), - array('word1 author:word2', 'word2'), - array('word1 author:word2 word3', 'word2'), - array('word1 author:"word2 word3"', 'word2 word3'), - array("word1 author:'word2 word3'", 'word2 word3'), - array('author:word1 author:word2', 'word1'), - array('author: word1 word2', ''), - array('author:123', '123'), - array('author:"word1 word2" word3"', 'word1 word2'), - array("author:'word1 word2' word3'", 'word1 word2'), - array('author:"word1 word2\' word3"', "word1 word2' word3"), - array("author:'word1 word2\" word3'", 'word1 word2" word3'), - ); - } - - /** - * @dataProvider provideInurlSearch - * @param string $search - * @param string $expected_value - */ - public function testParseSearch_whenInurlKeyword_returnArrayWithInurlValue($search, $expected_value) { - FreshRSS_Context::$search = $search; - $parsed_search = $this->context->parseSearch(); - $this->assertEquals($expected_value, $parsed_search['inurl']); - } - - /** - * @return array - */ - public function provideInurlSearch() { - return array( - array('inurl:word1', 'word1'), - array('inurl: word1', ''), - array('inurl:123', '123'), - array('inurl:word1 word2', 'word1'), - array('inurl:"word1 word2"', '"word1'), - ); - } - - /** - * @dataProvider provideDateSearch - * @param string $search - * @param string $expected_min_value - * @param string $expected_max_value - */ - public function testParseSearch_whenDateKeyword_returnArrayWithDateValues($search, $expected_min_value, $expected_max_value) { - FreshRSS_Context::$search = $search; - $parsed_search = $this->context->parseSearch(); - $this->assertEquals($expected_min_value, $parsed_search['min_date']); - $this->assertEquals($expected_max_value, $parsed_search['max_date']); - } - - /** - * @return array - */ - public function provideDateSearch() { - return array( - array('date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'), - array('date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'), - array('date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'), - array('date:2007-03-01/2008-05-11', '1172725200', '1210564799'), - array('date:2007-03-01/', '1172725200', ''), - array('date:/2008-05-11', '', '1210564799'), - ); - } - - /** - * @dataProvider providePubdateSearch - * @param string $search - * @param string $expected_min_value - * @param string $expected_max_value - */ - public function testParseSearch_whenPubdateKeyword_returnArrayWithPubdateValues($search, $expected_min_value, $expected_max_value) { - FreshRSS_Context::$search = $search; - $parsed_search = $this->context->parseSearch(); - $this->assertEquals($expected_min_value, $parsed_search['min_pubdate']); - $this->assertEquals($expected_max_value, $parsed_search['max_pubdate']); - } - - /** - * @return array - */ - public function providePubdateSearch() { - return array( - array('pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'), - array('pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'), - array('pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'), - array('pubdate:2007-03-01/2008-05-11', '1172725200', '1210564799'), - array('pubdate:2007-03-01/', '1172725200', ''), - array('pubdate:/2008-05-11', '', '1210564799'), - ); - } - } diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php new file mode 100644 index 000000000..6ddfc0370 --- /dev/null +++ b/tests/app/Models/SearchTest.php @@ -0,0 +1,276 @@ +assertNull($search->getRawInput()); + $this->assertNull($search->getIntitle()); + $this->assertNull($search->getMinDate()); + $this->assertNull($search->getMaxDate()); + $this->assertNull($search->getMinPubdate()); + $this->assertNull($search->getMaxPubdate()); + $this->assertNull($search->getAuthor()); + $this->assertNull($search->getTags()); + $this->assertNull($search->getSearch()); + } + + /** + * Return an array of values for the search object. + * Here is the description of the values + * @return array + */ + public function provideEmptyInput() { + return array( + array(''), + array(null), + ); + } + + /** + * @dataProvider provideIntitleSearch + * @param string $input + * @param string $intitle_value + * @param string|null $search_value + */ + public function test__construct_whenInputContainsIntitle_setsIntitlePropery($input, $intitle_value, $search_value) { + $search = new FreshRSS_Search($input); + $this->assertEquals($intitle_value, $search->getIntitle()); + $this->assertEquals($search_value, $search->getSearch()); + } + + /** + * @return array + */ + public function provideIntitleSearch() { + return array( + array('intitle:word1', 'word1', null), + array('intitle:word1 word2', 'word1', '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('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' word3", null), + array("intitle:'word1 word2\" word3'", 'word1 word2" word3', null), + ); + } + + /** + * @dataProvider provideAuthorSearch + * @param string $input + * @param string $author_value + * @param string|null $search_value + */ + public function test__construct_whenInputContainsAuthor_setsAuthorValue($input, $author_value, $search_value) { + $search = new FreshRSS_Search($input); + $this->assertEquals($author_value, $search->getAuthor()); + $this->assertEquals($search_value, $search->getSearch()); + } + + /** + * @return array + */ + public function provideAuthorSearch() { + return array( + array('author:word1', 'word1', null), + array('author:word1 word2', 'word1', '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('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' word3", null), + array("author:'word1 word2\" word3'", 'word1 word2" word3', null), + ); + } + + /** + * @dataProvider provideInurlSearch + * @param string $input + * @param string $inurl_value + * @param string|null $search_value + */ + public function test__construct_whenInputContainsInurl_setsInurlValue($input, $inurl_value, $search_value) { + $search = new FreshRSS_Search($input); + $this->assertEquals($inurl_value, $search->getInurl()); + $this->assertEquals($search_value, $search->getSearch()); + } + + /** + * @return array + */ + public function provideInurlSearch() { + return array( + array('inurl:word1', 'word1', null), + array('inurl: word1', null, 'word1'), + array('inurl:123', '123', null), + array('inurl:word1 word2', 'word1', 'word2'), + array('inurl:"word1 word2"', '"word1', 'word2"'), + ); + } + + /** + * @dataProvider provideDateSearch + * @param string $input + * @param string $min_date_value + * @param string $max_date_value + */ + public function test__construct_whenInputContainsDate_setsDateValues($input, $min_date_value, $max_date_value) { + $search = new FreshRSS_Search($input); + $this->assertEquals($min_date_value, $search->getMinDate()); + $this->assertEquals($max_date_value, $search->getMaxDate()); + } + + /** + * @return array + */ + public function provideDateSearch() { + return array( + array('date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'), + array('date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'), + array('date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'), + array('date:2007-03-01/2008-05-11', '1172725200', '1210564799'), + array('date:2007-03-01/', '1172725200', ''), + array('date:/2008-05-11', '', '1210564799'), + ); + } + + /** + * @dataProvider providePubdateSearch + * @param string $input + * @param string $min_pubdate_value + * @param string $max_pubdate_value + */ + public function test__construct_whenInputContainsPubdate_setsPubdateValues($input, $min_pubdate_value, $max_pubdate_value) { + $search = new FreshRSS_Search($input); + $this->assertEquals($min_pubdate_value, $search->getMinPubdate()); + $this->assertEquals($max_pubdate_value, $search->getMaxPubdate()); + } + + /** + * @return array + */ + public function providePubdateSearch() { + return array( + array('pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'), + array('pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'), + array('pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'), + array('pubdate:2007-03-01/2008-05-11', '1172725200', '1210564799'), + array('pubdate:2007-03-01/', '1172725200', ''), + array('pubdate:/2008-05-11', '', '1210564799'), + ); + } + + /** + * @dataProvider provideTagsSearch + * @param string $input + * @param string $tags_value + * @param string|null $search_value + */ + public function test__construct_whenInputContainsTags_setsTagsValue($input, $tags_value, $search_value) { + $search = new FreshRSS_Search($input); + $this->assertEquals($tags_value, $search->getTags()); + $this->assertEquals($search_value, $search->getSearch()); + } + + /** + * @return array + */ + public function provideTagsSearch() { + return array( + array('#word1', array('word1'), null), + array('# word1', null, '# word1'), + array('#123', array('123'), null), + array('#word1 word2', array('word1'), 'word2'), + array('#"word1 word2"', array('"word1'), 'word2"'), + array('#word1 #word2', array('word1', 'word2'), null), + ); + } + + /** + * @dataProvider provideMultipleSearch + * @param string $input + * @param string $author_value + * @param string $min_date_value + * @param string $max_date_value + * @param string $intitle_value + * @param string $inurl_value + * @param string $min_pubdate_value + * @param string $max_pubdate_value + * @param array $tags_value + * @param string|null $search_value + */ + public function test__construct_whenInputContainsMultipleKeywords_setsValues($input, $author_value, $min_date_value, $max_date_value, $intitle_value, $inurl_value, $min_pubdate_value, $max_pubdate_value, $tags_value, $search_value) { + $search = new FreshRSS_Search($input); + $this->assertEquals($author_value, $search->getAuthor()); + $this->assertEquals($min_date_value, $search->getMinDate()); + $this->assertEquals($max_date_value, $search->getMaxDate()); + $this->assertEquals($intitle_value, $search->getIntitle()); + $this->assertEquals($inurl_value, $search->getInurl()); + $this->assertEquals($min_pubdate_value, $search->getMinPubdate()); + $this->assertEquals($max_pubdate_value, $search->getMaxPubdate()); + $this->assertEquals($tags_value, $search->getTags()); + $this->assertEquals($search_value, $search->getSearch()); + } + + public function provideMultipleSearch() { + return array( + array( + 'author:word1 date:2007-03-01/2008-05-11 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 #word5', + 'word1', + '1172725200', + '1210564799', + 'word2', + 'word3', + '1172725200', + '1210564799', + array('word4', 'word5'), + null, + ), + array( + 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 date:2007-03-01/2008-05-11', + 'word1', + '1172725200', + '1210564799', + 'word2', + 'word3', + '1172725200', + '1210564799', + array('word4', 'word5'), + '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', + 'word1', + '1172725200', + '1210564799', + 'word2', + 'word3', + '1172725200', + '1210564799', + array('word4', 'word5'), + 'word6 word7', + ), + ); + } + +} -- cgit v1.2.3 From 9cee5c1a17947d3f3d10554844b7089e28cf8500 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 11 Feb 2015 21:52:10 -0500 Subject: Remove code generated by netbeans --- app/Models/Search.php | 2 -- 1 file changed, 2 deletions(-) (limited to 'app/Models') diff --git a/app/Models/Search.php b/app/Models/Search.php index 0475bc685..e64f6cb88 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -5,8 +5,6 @@ * * It allows to extract meaningful bits of the search and store them in a * convenient object - * - * @author alexis */ class FreshRSS_Search { -- cgit v1.2.3 From 9f83aa5fe79618be98cb027bb1070f5a11c51723 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Thu, 12 Feb 2015 21:05:33 -0500 Subject: Refactor the code to make less unnecessaty calls There were multiple calls made to the cleaning method that were unnecessary since it is useful only on the last call. It allows to simplify code by returning values ealier. --- app/Models/Search.php | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) (limited to 'app/Models') diff --git a/app/Models/Search.php b/app/Models/Search.php index e64f6cb88..ef8fc883d 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -32,7 +32,7 @@ class FreshRSS_Search { $input = $this->parsePubdateSearch($input); $input = $this->parseDateSearch($input); $input = $this->parseTagsSeach($input); - $this->search = $input; + $this->search = $this->cleanSearch($input); } public function getRawInput() { @@ -86,12 +86,13 @@ class FreshRSS_Search { private function parseIntitleSearch($input) { if (preg_match('/intitle:(?P[\'"])(?P.*)(?P=delim)/U', $input, $matches)) { $this->intitle = $matches['search']; - $input = str_replace($matches[0], '', $input); - } else if (preg_match('/intitle:(?P\w*)/', $input, $matches)) { + return str_replace($matches[0], '', $input); + } + if (preg_match('/intitle:(?P\w*)/', $input, $matches)) { $this->intitle = $matches['search']; - $input = str_replace($matches[0], '', $input); + return str_replace($matches[0], '', $input); } - return $this->cleanSearch($input); + return $input; } /** @@ -107,12 +108,13 @@ class FreshRSS_Search { private function parseAuthorSearch($input) { if (preg_match('/author:(?P[\'"])(?P.*)(?P=delim)/U', $input, $matches)) { $this->author = $matches['search']; - $input = str_replace($matches[0], '', $input); - } else if (preg_match('/author:(?P\w*)/', $input, $matches)) { + return str_replace($matches[0], '', $input); + } + if (preg_match('/author:(?P\w*)/', $input, $matches)) { $this->author = $matches['search']; - $input = str_replace($matches[0], '', $input); + return str_replace($matches[0], '', $input); } - return $this->cleanSearch($input); + return $input; } /** @@ -126,9 +128,9 @@ class FreshRSS_Search { private function parseInurlSearch($input) { if (preg_match('/inurl:(?P[^\s]*)/', $input, $matches)) { $this->inurl = $matches['search']; - $input = str_replace($matches[0], '', $input); + return str_replace($matches[0], '', $input); } - return $this->cleanSearch($input); + return $input; } /** @@ -142,9 +144,9 @@ class FreshRSS_Search { private function parseDateSearch($input) { if (preg_match('/date:(?P[^\s]*)/', $input, $matches)) { list($this->min_date, $this->max_date) = parseDateInterval($matches['search']); - $input = str_replace($matches[0], '', $input); + return str_replace($matches[0], '', $input); } - return $this->cleanSearch($input); + return $input; } /** @@ -158,9 +160,9 @@ class FreshRSS_Search { private function parsePubdateSearch($input) { if (preg_match('/pubdate:(?P[^\s]*)/', $input, $matches)) { list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($matches['search']); - $input = str_replace($matches[0], '', $input); + return str_replace($matches[0], '', $input); } - return $this->cleanSearch($input); + return $input; } /** @@ -174,11 +176,17 @@ class FreshRSS_Search { private function parseTagsSeach($input) { if (preg_match_all('/#(?P[^\s]+)/', $input, $matches)) { $this->tags = $matches['search']; - $input = str_replace($matches[0], '', $input); + return str_replace($matches[0], '', $input); } - return $this->cleanSearch($input); + return $input; } + /** + * Remove all unnecessary spaces in the search + * + * @param string $input + * @return string + */ private function cleanSearch($input) { $input = preg_replace('/\s+/', ' ', $input); return trim($input); -- cgit v1.2.3 From b8fd3caf8306e8616fcb2f2c0add95b74c2ec024 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 14 Feb 2015 11:00:26 -0500 Subject: Harmonize share configuration view. Before, for shares that don't need options, only a button to remove it was visible. It was source of confusion for users. I changed the look of those shares by using the same layout as others (minus the help). As there is no configuration possible for the url, the field is disabled but it is possible to change the name of the share. See #787 --- app/Models/Share.php | 2 +- app/i18n/de/gen.php | 1 + app/i18n/en/gen.php | 1 + app/i18n/fr/gen.php | 1 + app/views/configure/sharing.phtml | 20 +++++++++++--------- p/scripts/main.js | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) (limited to 'app/Models') diff --git a/app/Models/Share.php b/app/Models/Share.php index db6feda19..2a05f2ee9 100644 --- a/app/Models/Share.php +++ b/app/Models/Share.php @@ -152,7 +152,7 @@ class FreshRSS_Share { * Return the current name of the share option. */ public function name($real = false) { - if ($real || is_null($this->custom_name)) { + if ($real || is_null($this->custom_name) || empty($this->custom_name)) { return $this->name; } else { return $this->custom_name; diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index f3479ed53..3170b29c2 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -156,6 +156,7 @@ return array( 'damn' => 'Verdammt!', 'default_category' => 'Unkategorisiert', 'no' => 'Nein', + 'not_applicable' => 'N/A', 'ok' => 'OK!', 'or' => 'oder', 'yes' => 'Ja', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 2143822ed..420e73f36 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -156,6 +156,7 @@ return array( 'damn' => 'Damn!', 'default_category' => 'Uncategorized', 'no' => 'No', + 'not_applicable' => 'N/A', 'ok' => 'Ok!', 'or' => 'or', 'yes' => 'Yes', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 1cfec6969..ae946ce0c 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -156,6 +156,7 @@ return array( 'damn' => 'Arf !', 'default_category' => 'Sans catégorie', 'no' => 'Non', + 'not_applicable' => 'N/A', 'ok' => 'Ok !', 'or' => 'ou', 'yes' => 'Oui', diff --git a/app/views/configure/sharing.phtml b/app/views/configure/sharing.phtml index da7557480..deb1ed6b7 100644 --- a/app/views/configure/sharing.phtml +++ b/app/views/configure/sharing.phtml @@ -4,7 +4,8 @@
+ data-simple='
+
' data-advanced='
@@ -26,16 +27,17 @@
+
+ formType() === 'advanced') { ?> -
- - - -
- - + - + + + +
+ formType() === 'advanced') { ?> +
diff --git a/p/scripts/main.js b/p/scripts/main.js index 1be75bb12..7fb583d39 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -1097,7 +1097,7 @@ function init_share_observers() { $('.share.add').on('click', function(e) { var opt = $(this).siblings('select').find(':selected'); var row = $(this).parents('form').data(opt.data('form')); - row = row.replace('##label##', opt.html(), 'g'); + row = row.replace('##label##', opt.html().trim(), 'g'); row = row.replace('##type##', opt.val(), 'g'); row = row.replace('##help##', opt.data('help'), 'g'); row = row.replace('##key##', shares, 'g'); -- cgit v1.2.3 From f5028d30d047ae50ac2d89a9a66266de086ac718 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 21 Feb 2015 07:53:45 -0500 Subject: Add required library The lib_date.php library was missing in the Search object file. It is required to make date conversion in that object. --- app/Models/Search.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/Models') diff --git a/app/Models/Search.php b/app/Models/Search.php index ef8fc883d..a22e242af 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -1,5 +1,7 @@ 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/Controllers/indexController.php | 2 +- app/Models/Context.php | 2 +- app/Models/EntryDAO.php | 84 +++++++++++++++++-------------------- 3 files changed, 40 insertions(+), 48 deletions(-) (limited to 'app/Models') diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index c53d3223e..c1aaca53f 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -173,7 +173,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ; } - FreshRSS_Context::$search = Minz_Request::param('search', ''); + FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', '')); FreshRSS_Context::$order = Minz_Request::param( 'order', FreshRSS_Context::$user_conf->sort_order ); diff --git a/app/Models/Context.php b/app/Models/Context.php index f00bb1e97..dbdbfaa69 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -30,7 +30,7 @@ class FreshRSS_Context { public static $state = 0; public static $order = 'DESC'; public static $number = 0; - public static $search = ''; + public static $search; public static $first_id = ''; public static $next_id = ''; public static $id_max = ''; 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 50b6a02578c29c780e6fe36af712bdc7cfe81d3d Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 21 Feb 2015 09:42:06 -0500 Subject: Add search default raw value Before, the default value was undefined. Now it always has a value even when no value is provided. --- app/Models/Search.php | 2 +- tests/app/Models/SearchTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app/Models') diff --git a/app/Models/Search.php b/app/Models/Search.php index a22e242af..85340ff13 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -11,7 +11,7 @@ require_once(LIB_PATH . '/lib_date.php'); class FreshRSS_Search { // This contains the user input string - private $raw_input; + private $raw_input = ''; // The following properties are extracted from the raw input private $intitle; private $min_date; diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php index 20ea09433..9e3ca6765 100644 --- a/tests/app/Models/SearchTest.php +++ b/tests/app/Models/SearchTest.php @@ -10,7 +10,7 @@ class SearchTest extends \PHPUnit_Framework_TestCase { */ public function test__construct_whenInputIsEmpty_getsOnlyNullValues($input) { $search = new FreshRSS_Search($input); - $this->assertNull($search->getRawInput()); + $this->assertEquals('', $search->getRawInput()); $this->assertNull($search->getIntitle()); $this->assertNull($search->getMinDate()); $this->assertNull($search->getMaxDate()); -- cgit v1.2.3 From 74c020edc846d0afdecb05ae1e1f30a516dee002 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 21 Feb 2015 09:44:02 -0500 Subject: Add a default string representation In some part of the code, the search is displayed as is and crash since there is no way to display the search object as a string. --- app/Models/Search.php | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app/Models') diff --git a/app/Models/Search.php b/app/Models/Search.php index 85340ff13..84688be2e 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -36,6 +36,10 @@ class FreshRSS_Search { $input = $this->parseTagsSeach($input); $this->search = $this->cleanSearch($input); } + + public function __toString() { + return $this->getRawInput(); + } public function getRawInput() { return $this->raw_input; -- 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') 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') 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') 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 96d5d9d034daadb2357f27b3763854b647f2086c Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Thu, 5 Mar 2015 06:45:00 -0500 Subject: Fix DAO exception Change the name and messages --- app/Exceptions/DAOException.php | 2 +- app/Models/UserQuery.php | 8 ++++---- tests/app/Models/UserQueryTest.php | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'app/Models') diff --git a/app/Exceptions/DAOException.php b/app/Exceptions/DAOException.php index 6bd8f4ff0..e48e521ab 100644 --- a/app/Exceptions/DAOException.php +++ b/app/Exceptions/DAOException.php @@ -1,5 +1,5 @@ category_dao)) { - throw new FreshRSS_DAOException('Category DAO is not loaded i UserQuery'); + throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery'); } $category = $this->category_dao->searchById($id); if ($category) { @@ -123,11 +123,11 @@ class FreshRSS_UserQuery { * Parse the query string when it is a "feed" query * * @param integer $id - * @throws FreshRSS_DAOException + * @throws FreshRSS_DAO_Exception */ private function parseFeed($id) { if (is_null($this->feed_dao)) { - throw new FreshRSS_DAOException('Feed DAO is not loaded i UserQuery'); + throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery'); } $feed = $this->feed_dao->searchById($id); if ($feed) { diff --git a/tests/app/Models/UserQueryTest.php b/tests/app/Models/UserQueryTest.php index 2234be6e1..a0928d5ae 100644 --- a/tests/app/Models/UserQueryTest.php +++ b/tests/app/Models/UserQueryTest.php @@ -20,7 +20,7 @@ class UserQueryTest extends \PHPUnit_Framework_TestCase { } /** - * @expectedException Exceptions/FreshRSS_DAOException + * @expectedException Exceptions/FreshRSS_DAO_Exception * @expectedExceptionMessage Category DAO is not loaded in UserQuery */ public function test__construct_whenCategoryQueryAndNoDao_throwsException() { @@ -48,7 +48,7 @@ class UserQueryTest extends \PHPUnit_Framework_TestCase { } /** - * @expectedException Exceptions/FreshRSS_DAOException + * @expectedException Exceptions/FreshRSS_DAO_Exception * @expectedExceptionMessage Feed DAO is not loaded in UserQuery */ public function test__construct_whenFeedQueryAndNoDao_throwsException() { -- 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') 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 1a35e2271d3b9383e882371d37d5fef16abd745d Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 21 Mar 2015 18:20:36 +0100 Subject: SimplePie option to restaure syslog of HTTP requests https://github.com/FreshRSS/FreshRSS/issues/711 --- app/Models/Feed.php | 4 ++-- data/config.default.php | 1 + lib/SimplePie/SimplePie.php | 41 ++++++++++++++++++++++++++++++++++------ lib/SimplePie/SimplePie/File.php | 7 +++++-- lib/lib_rss.php | 1 + 5 files changed, 44 insertions(+), 10 deletions(-) (limited to 'app/Models') diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 5ce03be5d..5f67ea6ce 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -246,10 +246,10 @@ class FreshRSS_Feed extends Minz_Model { } if (($mtime === true) ||($mtime > $this->lastUpdate)) { - Minz_Log::notice('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url); + //Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url); $this->loadEntries($feed); // et on charge les articles du flux } else { - Minz_Log::notice('FreshRSS use cache for ' . $clean_url); + //Minz_Log::debug('FreshRSS use cache for ' . $clean_url); $this->entries = array(); } diff --git a/data/config.default.php b/data/config.default.php index 97df3a299..839bd1687 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -12,6 +12,7 @@ return array( 'auth_type' => 'none', 'api_enabled' => false, 'unsafe_autologin_enabled' => false, + 'simplepie_syslog_enabled' => true, 'limits' => array( 'cache_duration' => 800, 'timeout' => 10, diff --git a/lib/SimplePie/SimplePie.php b/lib/SimplePie/SimplePie.php index c4872b5be..bb8ce4191 100644 --- a/lib/SimplePie/SimplePie.php +++ b/lib/SimplePie/SimplePie.php @@ -74,6 +74,12 @@ define('SIMPLEPIE_USERAGENT', SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION . ' (Feed */ define('SIMPLEPIE_LINKBACK', '' . SIMPLEPIE_NAME . ''); +/** + * Use syslog to report HTTP requests done by SimplePie. + * @see SimplePie::set_syslog() + */ +define('SIMPLEPIE_SYSLOG', true); //FreshRSS + /** * No Autodiscovery * @see SimplePie::set_autodiscovery_level() @@ -622,6 +628,12 @@ class SimplePie */ public $strip_htmltags = array('base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style'); + /** + * Use syslog to report HTTP requests done by SimplePie. + * @see SimplePie::set_syslog() + */ + public $syslog_enabled = SIMPLEPIE_SYSLOG; + /** * The SimplePie class contains feed level data and options * @@ -1136,7 +1148,7 @@ class SimplePie $this->sanitize->strip_attributes($attribs); } - public function add_attributes($attribs = '') + public function add_attributes($attribs = '') //FreshRSS { if ($attribs === '') { @@ -1145,6 +1157,14 @@ class SimplePie $this->sanitize->add_attributes($attribs); } + /** + * Use syslog to report HTTP requests done by SimplePie. + */ + public function set_syslog($value = SIMPLEPIE_SYSLOG) //FreshRSS + { + $this->syslog_enabled = $value == true; + } + /** * Set the output encoding * @@ -1231,7 +1251,8 @@ class SimplePie $this->enable_exceptions = $enable; } - function cleanMd5($rss) { //FreshRSS + function cleanMd5($rss) //FreshRSS + { return md5(preg_replace(array('#<(lastBuildDate|pubDate|updated|feedDate|dc:date|slash:comments)>[^<]+#', '##s'), '', $rss)); } @@ -1329,7 +1350,8 @@ class SimplePie list($headers, $sniffed) = $fetched; - if (isset($this->data['md5'])) { //FreshRSS + if (isset($this->data['md5'])) //FreshRSS + { $md5 = $this->data['md5']; } } @@ -1455,7 +1477,8 @@ class SimplePie { // Load the Cache $this->data = $cache->load(); - if ($cache->mtime() + $this->cache_duration > time()) { //FreshRSS + if ($cache->mtime() + $this->cache_duration > time()) //FreshRSS + { $this->raw_data = false; return true; // If the cache is still valid, just return true } @@ -1529,11 +1552,17 @@ class SimplePie { //FreshRSS $md5 = $this->cleanMd5($file->body); if ($this->data['md5'] === $md5) { - // syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url); + if ($this->syslog_enabled) + { + syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url); + } $cache->touch(); return true; //Content unchanged even though server did not send a 304 } else { - // syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url); + if ($this->syslog_enabled) + { + syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url); + } $this->data['md5'] = $md5; } } diff --git a/lib/SimplePie/SimplePie/File.php b/lib/SimplePie/SimplePie/File.php index 9625af2a9..56fe72196 100644 --- a/lib/SimplePie/SimplePie/File.php +++ b/lib/SimplePie/SimplePie/File.php @@ -66,7 +66,7 @@ class SimplePie_File var $method = SIMPLEPIE_FILE_SOURCE_NONE; var $permanent_url; //FreshRSS - public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false) + public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $syslog_enabled = SIMPLEPIE_SYSLOG) { if (class_exists('idna_convert')) { @@ -79,7 +79,10 @@ class SimplePie_File $this->useragent = $useragent; if (preg_match('/^http(s)?:\/\//i', $url)) { - // syslog(LOG_INFO, 'SimplePie GET ' . $url); //FreshRSS + if ($syslog_enabled) + { + syslog(LOG_INFO, 'SimplePie GET ' . $url); //FreshRSS + } if ($useragent === null) { $useragent = ini_get('user_agent'); diff --git a/lib/lib_rss.php b/lib/lib_rss.php index e5fe73041..16ae3097f 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -123,6 +123,7 @@ function customSimplePie() { $limits = $system_conf->limits; $simplePie = new SimplePie(); $simplePie->set_useragent(_t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION); + $simplePie->set_syslog($system_conf->simplepie_syslog_enabled); $simplePie->set_cache_location(CACHE_PATH); $simplePie->set_cache_duration($limits['cache_duration']); $simplePie->set_timeout($limits['timeout']); -- cgit v1.2.3 From ad9fe52f5a76faf58d13fcf7bde8f58e85abe82b Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 22 Mar 2015 22:54:29 +0100 Subject: SimplePie sanitize URLs for syslog https://github.com/FreshRSS/FreshRSS/issues/711 https://github.com/FreshRSS/FreshRSS/pull/715 --- app/Models/Feed.php | 2 +- lib/SimplePie/SimplePie.php | 4 ++-- lib/SimplePie/SimplePie/File.php | 2 +- lib/SimplePie/SimplePie/Misc.php | 10 ++++++++++ lib/lib_rss.php | 12 +----------- 5 files changed, 15 insertions(+), 15 deletions(-) (limited to 'app/Models') diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 5f67ea6ce..15cbb7d0a 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -240,7 +240,7 @@ class FreshRSS_Feed extends Minz_Model { $subscribe_url = $feed->subscribe_url(true); } - $clean_url = url_remove_credentials($subscribe_url); + $clean_url = SimplePie_Misc::url_remove_credentials($subscribe_url); if ($subscribe_url !== null && $subscribe_url !== $url) { $this->_url($clean_url); } diff --git a/lib/SimplePie/SimplePie.php b/lib/SimplePie/SimplePie.php index bb8ce4191..54f4c5770 100644 --- a/lib/SimplePie/SimplePie.php +++ b/lib/SimplePie/SimplePie.php @@ -1554,14 +1554,14 @@ class SimplePie if ($this->data['md5'] === $md5) { if ($this->syslog_enabled) { - syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url); + syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url)); } $cache->touch(); return true; //Content unchanged even though server did not send a 304 } else { if ($this->syslog_enabled) { - syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url); + syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url)); } $this->data['md5'] = $md5; } diff --git a/lib/SimplePie/SimplePie/File.php b/lib/SimplePie/SimplePie/File.php index 56fe72196..1f9e3d502 100644 --- a/lib/SimplePie/SimplePie/File.php +++ b/lib/SimplePie/SimplePie/File.php @@ -81,7 +81,7 @@ class SimplePie_File { if ($syslog_enabled) { - syslog(LOG_INFO, 'SimplePie GET ' . $url); //FreshRSS + syslog(LOG_INFO, 'SimplePie GET ' . SimplePie_Misc::url_remove_credentials($url)); //FreshRSS } if ($useragent === null) { diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php index 5a263a2e5..de50d37b8 100644 --- a/lib/SimplePie/SimplePie/Misc.php +++ b/lib/SimplePie/SimplePie/Misc.php @@ -2240,5 +2240,15 @@ function embed_wmedia(width, height, link) { { // No-op } + + /** + * Sanitize a URL by removing HTTP credentials. + * @param $url the URL to sanitize. + * @return the same URL without HTTP credentials. + */ + function url_remove_credentials($url) //FreshRSS + { + return preg_replace('#(?<=//)[^/:@]+:[^/:@]+@#', '', $url); + } } diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 16ae3097f..65a1a8e04 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -181,7 +181,7 @@ function sanitizeHTML($data, $base = '') { function get_content_by_parsing ($url, $path) { require_once (LIB_PATH . '/lib_phpQuery.php'); - Minz_Log::notice('FreshRSS GET ' . url_remove_credentials($url)); + Minz_Log::notice('FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url)); $html = file_get_contents ($url); if ($html) { @@ -430,13 +430,3 @@ function array_push_unique(&$array, $value) { function array_remove(&$array, $value) { $array = array_diff($array, array($value)); } - - -/** - * Sanitize a URL by removing HTTP credentials. - * @param $url the URL to sanitize. - * @return the same URL without HTTP credentials. - */ -function url_remove_credentials($url) { - return preg_replace('/[^\/]*:[^:]*@/', '', $url); -} -- cgit v1.2.3 From 239a010ef23127429698b6be5f4a5453b0bfbad7 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 24 Mar 2015 22:45:27 +0100 Subject: Error when deleting a feed https://github.com/FreshRSS/FreshRSS/issues/816 --- app/Models/ConfigurationSetter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/Models') diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index d7689752f..978cc8cb9 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -117,7 +117,9 @@ class FreshRSS_ConfigurationSetter { private function _queries(&$data, $values) { $data['queries'] = array(); foreach ($values as $value) { - $data['queries'][] = $value->toArray(); + if ($value != null) { + $data['queries'][] = $value->toArray(); + } } } -- cgit v1.2.3 From d7706b66f586b36e44e471b5c06526de258af8b8 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 24 Mar 2015 22:51:51 +0100 Subject: Error when deleting a feed, wrong object https://github.com/FreshRSS/FreshRSS/issues/816 --- app/Models/ConfigurationSetter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/Models') diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 978cc8cb9..7f433239c 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -117,7 +117,7 @@ class FreshRSS_ConfigurationSetter { private function _queries(&$data, $values) { $data['queries'] = array(); foreach ($values as $value) { - if ($value != null) { + if ($value instanceof FreshRSS_UserQuery) { $data['queries'][] = $value->toArray(); } } -- 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') 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') 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') 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 993466844405bd9854d890d6d5ebf763ed8b78cb Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 9 May 2015 23:37:56 +0200 Subject: SQL: more robust purge https://github.com/FreshRSS/FreshRSS/issues/798 https://github.com/FreshRSS/FreshRSS/issues/493 --- app/Controllers/feedController.php | 13 +++++-------- app/Models/Feed.php | 3 +-- app/Models/FeedDAO.php | 8 +++++--- 3 files changed, 11 insertions(+), 13 deletions(-) (limited to 'app/Models') diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 59c9174fb..5657d4a88 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -329,7 +329,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // 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. @@ -353,14 +352,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // 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. + if ($entry_date < $date_min) { $id = min(time(), $entry_date) . uSecString(); + $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read + } else { + $id = uTimeString(); + $entry->_isRead($is_read); } - $entry->_id($id); - $entry->_isRead($is_read); $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); if ($entry === null) { @@ -376,7 +375,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } $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) @@ -384,7 +382,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { 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, diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 27c83ffd5..5d377de9a 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -245,7 +245,7 @@ class FreshRSS_Feed extends Minz_Model { $this->_url($clean_url); } - if (($mtime === true) ||($mtime > $this->lastUpdate)) { + if (($mtime === true) || ($mtime > $this->lastUpdate)) { Minz_Log::notice('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url); $this->loadEntries($feed); // et on charge les articles du flux } else { @@ -255,7 +255,6 @@ 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/Models/FeedDAO.php b/app/Models/FeedDAO.php index f48beee6e..c13e2b008 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -322,10 +322,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $affected; } - public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) just after + public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) or updateCachedValues() just after $sql = 'DELETE FROM `' . $this->prefix . 'entry` ' - . 'WHERE id_feed = :id_feed AND id <= :id_max AND is_favorite=0 AND id NOT IN ' - . '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' + . 'WHERE id_feed = :id_feed AND id <= :id_max ' + . 'AND is_favorite=0 ' //Do not remove favourites + . 'AND lastSeen < (SELECT maxLastSeen FROM (SELECT (MAX(e3.lastSeen) - 99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed = :id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance + . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery' $stm = $this->bd->prepare($sql); $id_max = intval($date_min) . '000000'; -- cgit v1.2.3 From a7bc54bb996a87a66127101be548d181dc8dd935 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 9 May 2015 23:45:52 +0200 Subject: Minor spaces --- app/Models/FeedDAO.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app/Models') diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index c13e2b008..76025ff53 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -324,10 +324,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) or updateCachedValues() just after $sql = 'DELETE FROM `' . $this->prefix . 'entry` ' - . 'WHERE id_feed = :id_feed AND id <= :id_max ' + . 'WHERE id_feed=:id_feed AND id<=:id_max ' . 'AND is_favorite=0 ' //Do not remove favourites - . 'AND lastSeen < (SELECT maxLastSeen FROM (SELECT (MAX(e3.lastSeen) - 99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed = :id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance - . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery' + . 'AND lastSeen < (SELECT maxLastSeen FROM (SELECT (MAX(e3.lastSeen)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance + . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery' $stm = $this->bd->prepare($sql); $id_max = intval($date_min) . '000000'; -- cgit v1.2.3 From 0d0c6b7493161d350ca2eb1d4c45c0c70cfcbb92 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 14:04:12 +0200 Subject: Moved updated/unread option from global to user https://github.com/FreshRSS/FreshRSS/issues/798 --- app/Controllers/configureController.php | 1 + app/Controllers/feedController.php | 2 +- app/Models/ConfigurationSetter.php | 4 ++++ app/i18n/de/conf.php | 1 + app/i18n/en/conf.php | 1 + app/i18n/fr/conf.php | 1 + app/views/configure/reading.phtml | 9 +++++++++ data/config.default.php | 4 ---- data/users/_/config.default.php | 5 +++++ 9 files changed, 23 insertions(+), 5 deletions(-) (limited to 'app/Models') diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index fc92aa0c2..248a3edcc 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -112,6 +112,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false); FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false); FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false); + FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', false); FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC'); FreshRSS_Context::$user_conf->mark_when = array( 'article' => Minz_Request::param('mark_open_article', false), diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 03f438888..a36a38ce2 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -343,7 +343,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() . ', old hash ' . $existingHash . ', new hash ' . $entry->hash()); //TODO: Make an updated/is_read policy by feed, in addition to the global one. - $entry->_isRead(FreshRSS_Context::$system_conf->mark_updated_article_unread ? false : null); //Change is_read according to policy. + $entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null); //Change is_read according to policy. if (!$entryDAO->hasTransaction()) { $entryDAO->beginTransaction(); } diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 7f433239c..4bd29ecb0 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -189,6 +189,10 @@ class FreshRSS_ConfigurationSetter { $data['auto_remove_article'] = $this->handleBool($value); } + private function _mark_updated_article_unread(&$data, $value) { + $data['mark_updated_article_unread'] = $this->handleBool($value); + } + private function _display_categories(&$data, $value) { $data['display_categories'] = $this->handleBool($value); } diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php index 64c2c0945..df2c07d49 100644 --- a/app/i18n/de/conf.php +++ b/app/i18n/de/conf.php @@ -84,6 +84,7 @@ return array( 'articles_per_page' => 'Anzahl der Artikel pro Seite', 'auto_load_more' => 'Die nächsten Artikel am Seitenende laden', 'auto_remove_article' => 'Artikel nach dem Lesen verstecken', + 'mark_updated_article_unread' => 'Markieren Sie aktualisierte Artikel als ungelesen', 'confirm_enabled' => 'Bei der Aktion „Alle als gelesen markieren“ einen Bestätigungsdialog anzeigen', 'display_articles_unfolded' => 'Artikel standardmäßig ausgeklappt zeigen', 'display_categories_unfolded' => 'Kategorien standardmäßig eingeklappt zeigen', diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php index 308c45d2c..683781696 100644 --- a/app/i18n/en/conf.php +++ b/app/i18n/en/conf.php @@ -84,6 +84,7 @@ return array( 'articles_per_page' => 'Number of articles per page', 'auto_load_more' => 'Load next articles at the page bottom', 'auto_remove_article' => 'Hide articles after reading', + 'mark_updated_article_unread' => 'Mark updated articles as unread', 'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions', 'display_articles_unfolded' => 'Show articles unfolded by default', 'display_categories_unfolded' => 'Show categories folded by default', diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php index d38445b99..87f9be290 100644 --- a/app/i18n/fr/conf.php +++ b/app/i18n/fr/conf.php @@ -84,6 +84,7 @@ return array( 'articles_per_page' => 'Nombre d’articles par page', 'auto_load_more' => 'Charger les articles suivants en bas de page', 'auto_remove_article' => 'Cacher les articles après lecture', + 'mark_updated_article_unread' => 'Marquer les articles mis à jour comme non-lus', 'confirm_enabled' => 'Afficher une confirmation lors des actions “marquer tout comme lu”', 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', 'display_categories_unfolded' => 'Afficher les catégories pliées par défaut', diff --git a/app/views/configure/reading.phtml b/app/views/configure/reading.phtml index 8b123afa8..1b7a101df 100644 --- a/app/views/configure/reading.phtml +++ b/app/views/configure/reading.phtml @@ -125,6 +125,15 @@
+
+
+ +
+
+
diff --git a/data/config.default.php b/data/config.default.php index dc947f154..8be203d36 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -55,10 +55,6 @@ return array( # SimplePie, which is retrieving RSS feeds via HTTP requests. 'simplepie_syslog_enabled' => true, - # In the case an article has changed (e.g. updated content): - # Set to `true` to mark it unread, or `false` to leave it as-is. - 'mark_updated_article_unread' => false, - 'limits' => array( # Duration in seconds of the SimplePie cache, diff --git a/data/users/_/config.default.php b/data/users/_/config.default.php index 6d3f73a13..bf74ca1de 100644 --- a/data/users/_/config.default.php +++ b/data/users/_/config.default.php @@ -22,6 +22,11 @@ return array ( 'sticky_post' => true, 'reading_confirm' => false, 'auto_remove_article' => false, + + # In the case an article has changed (e.g. updated content): + # Set to `true` to mark it unread, or `false` to leave it as-is. + 'mark_updated_article_unread' => false, + 'sort_order' => 'DESC', 'anon_access' => false, 'mark_when' => array ( -- 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') 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') 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') 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