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/Search.php | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 app/Models/Search.php (limited to 'app/Models/Search.php') 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); + } + +} -- 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/Search.php') 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/Search.php') 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 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/Search.php') 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: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/Search.php') 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/Search.php') 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 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/Search.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 9736d5cd3..5bdc216bc 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -478,11 +478,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } if ($filter->getSearch()) { - $search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? '; - $values[] = "%{$filter->getSearch()}%"; + $search_values = $filter->getSearch(); + foreach ($search_values as $search_value) { + $search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? '; + $values[] = "%{$search_value}%"; + } } } - return array($values, 'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 ' . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed=f.id ' : '') diff --git a/app/Models/Search.php b/app/Models/Search.php index 84688be2e..575a9a2cb 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -34,9 +34,9 @@ class FreshRSS_Search { $input = $this->parsePubdateSearch($input); $input = $this->parseDateSearch($input); $input = $this->parseTagsSeach($input); - $this->search = $this->cleanSearch($input); + $this->parseSearch($input); } - + public function __toString() { return $this->getRawInput(); } @@ -187,6 +187,34 @@ class FreshRSS_Search { return $input; } + /** + * Parse the search string to find search values. + * Every word is a distinct search value, except when using a delimiter. + * Supported delimiters are single quote (') and double quotes ("). + * + * @param string $input + * @return string + */ + private function parseSearch($input) { + $input = $this->cleanSearch($input); + if (strcmp($input, '') == 0) { + return; + } + if (preg_match_all('/(?P[\'"])(?P.*)(?P=delim)/U', $input, $matches)) { + $this->search = $matches['search']; + $input = str_replace($matches[0], '', $input); + } + $input = $this->cleanSearch($input); + if (strcmp($input, '') == 0) { + return; + } + if (is_array($this->search)) { + $this->search = array_merge($this->search, explode(' ', $input)); + } else { + $this->search = explode(' ', $input); + } + } + /** * Remove all unnecessary spaces in the search * diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php index 9e3ca6765..73ff56cc6 100644 --- a/tests/app/Models/SearchTest.php +++ b/tests/app/Models/SearchTest.php @@ -51,20 +51,21 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideIntitleSearch() { return array( array('intitle:word1', 'word1', null), - array('intitle:word1 word2', 'word1', 'word2'), + array('intitle:word1 word2', 'word1', array('word2')), array('intitle:"word1 word2"', 'word1 word2', null), array("intitle:'word1 word2'", 'word1 word2', null), - array('word1 intitle:word2', 'word2', 'word1'), - array('word1 intitle:word2 word3', 'word2', 'word1 word3'), - array('word1 intitle:"word2 word3"', 'word2 word3', 'word1'), - array("word1 intitle:'word2 word3'", 'word2 word3', 'word1'), - array('intitle:word1 intitle:word2', 'word1', 'intitle:word2'), - array('intitle: word1 word2', null, 'word1 word2'), + array('word1 intitle:word2', 'word2', array('word1')), + array('word1 intitle:word2 word3', 'word2', array('word1', 'word3')), + array('word1 intitle:"word2 word3"', 'word2 word3', array('word1')), + array("word1 intitle:'word2 word3'", 'word2 word3', array('word1')), + array('intitle:word1 intitle:word2', 'word1', array('intitle:word2')), + array('intitle: word1 word2', null, array('word1', 'word2')), array('intitle:123', '123', null), - array('intitle:"word1 word2" word3"', 'word1 word2', 'word3"'), - array("intitle:'word1 word2' word3'", 'word1 word2', "word3'"), + array('intitle:"word1 word2" word3"', 'word1 word2', array('word3"')), + array("intitle:'word1 word2' word3'", 'word1 word2', array("word3'")), array('intitle:"word1 word2\' word3"', "word1 word2' word3", null), array("intitle:'word1 word2\" word3'", 'word1 word2" word3', null), + array("intitle:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), ); } @@ -86,20 +87,21 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideAuthorSearch() { return array( array('author:word1', 'word1', null), - array('author:word1 word2', 'word1', 'word2'), + array('author:word1 word2', 'word1', array('word2')), array('author:"word1 word2"', 'word1 word2', null), array("author:'word1 word2'", 'word1 word2', null), - array('word1 author:word2', 'word2', 'word1'), - array('word1 author:word2 word3', 'word2', 'word1 word3'), - array('word1 author:"word2 word3"', 'word2 word3', 'word1'), - array("word1 author:'word2 word3'", 'word2 word3', 'word1'), - array('author:word1 author:word2', 'word1', 'author:word2'), - array('author: word1 word2', null, 'word1 word2'), + array('word1 author:word2', 'word2', array('word1')), + array('word1 author:word2 word3', 'word2', array('word1', 'word3')), + array('word1 author:"word2 word3"', 'word2 word3', array('word1')), + array("word1 author:'word2 word3'", 'word2 word3', array('word1')), + array('author:word1 author:word2', 'word1', array('author:word2')), + array('author: word1 word2', null, array('word1', 'word2')), array('author:123', '123', null), - array('author:"word1 word2" word3"', 'word1 word2', 'word3"'), - array("author:'word1 word2' word3'", 'word1 word2', "word3'"), + array('author:"word1 word2" word3"', 'word1 word2', array('word3"')), + array("author:'word1 word2' word3'", 'word1 word2', array("word3'")), array('author:"word1 word2\' word3"', "word1 word2' word3", null), array("author:'word1 word2\" word3'", 'word1 word2" word3', null), + array("author:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), ); } @@ -121,10 +123,11 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideInurlSearch() { return array( array('inurl:word1', 'word1', null), - array('inurl: word1', null, 'word1'), + array('inurl: word1', null, array('word1')), array('inurl:123', '123', null), - array('inurl:word1 word2', 'word1', 'word2'), - array('inurl:"word1 word2"', '"word1', 'word2"'), + array('inurl:word1 word2', 'word1', array('word2')), + array('inurl:"word1 word2"', '"word1', array('word2"')), + array("inurl:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), ); } @@ -198,11 +201,12 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideTagsSearch() { return array( array('#word1', array('word1'), null), - array('# word1', null, '# word1'), + array('# word1', null, array('#', 'word1')), array('#123', array('123'), null), - array('#word1 word2', array('word1'), 'word2'), - array('#"word1 word2"', array('"word1'), 'word2"'), + array('#word1 word2', array('word1'), array('word2')), + array('#"word1 word2"', array('"word1'), array('word2"')), array('#word1 #word2', array('word1', 'word2'), null), + array("#word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')), ); } @@ -257,7 +261,7 @@ class SearchTest extends \PHPUnit_Framework_TestCase { '1172725200', '1210564799', array('word4', 'word5'), - 'word6', + array('word6'), ), array( 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 word7 date:2007-03-01/2008-05-11', @@ -269,7 +273,19 @@ class SearchTest extends \PHPUnit_Framework_TestCase { '1172725200', '1210564799', array('word4', 'word5'), - 'word6 word7', + array('word6', 'word7'), + ), + array( + 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 "word7 word8" date:2007-03-01/2008-05-11', + 'word1', + '1172725200', + '1210564799', + 'word2', + 'word3', + '1172725200', + '1210564799', + array('word4', 'word5'), + array('word7 word8', 'word6'), ), ); } -- cgit v1.2.3