From 88f94661f3e4a7007938f237f3a74753446abeae Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Tue, 3 Feb 2015 10:32:41 +0100 Subject: [PATCH 01/90] New version number for dev (1.2-dev) --- CHANGELOG | 2 ++ constants.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b389f184b..d1b49d339 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # Changelog +## 2015-xx-xx FreshRSS 1.1.1 (beta) + ## 2015-01-31 FreshRSS 1.0.0 / 1.1.0 (beta) * UI diff --git a/constants.php b/constants.php index 8136afc85..b20bf0710 100644 --- a/constants.php +++ b/constants.php @@ -1,5 +1,5 @@ Date: Tue, 3 Feb 2015 10:36:45 +0100 Subject: [PATCH 02/90] Update CREDITS file --- CREDITS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CREDITS b/CREDITS index 47a968deb..c2c6642dc 100644 --- a/CREDITS +++ b/CREDITS @@ -29,6 +29,9 @@ dev@marienfressinaud.fr http://marienfressinaud.fr https://github.com/marienfressinaud +Melvyn Laïly +https://github.com/yaurthek + Nicolas Elie https://github.com/nicolaselie From 694212cce8d5538ace1e86096c0eb93907b947e5 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Tue, 3 Feb 2015 15:57:33 +0100 Subject: [PATCH 03/90] Create CONTRIBUTING.md file See https://github.com/FreshRSS/FreshRSS/issues/783 --- CONTRIBUTING.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fb1610a1f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# How to contribute to FreshRSS? + +## Report a bug + +You found a bug? Don't panic, here are some steps to report it easily: + +1. Search for it on [the bug tracker](https://github.com/FreshRSS/FreshRSS/issues) (don't forget to use the search bar). +2. If you find a similar bug, don't hesitate to post a comment to add more importance to the related ticket. +3. If you didn't find it, [open a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new). + +If you have to create a new ticket, try to apply the following advices: + +- Give an explicit title to the ticket so it will be easier to find it later. +- Be as exhaustive as possible in the description: what did you do? What is the bug? What are the steps to reproduce the bug? +- We also need some information: + + Your FreshRSS version (on about page or `constants.php` file) + + Your server configuration: type of hosting, PHP version + + Your storage system (MySQL / MariaDB or SQLite) + + If possible, the related logs (PHP logs and FreshRSS logs under `data/users/your_user/log.txt`) + +## Fix a bug + +Did you want to fix a bug? To keep a great coordination between collaborators, you will have to follow these indications: + +1. Be sure the bug is associated to a ticket and say you work on it. +2. Fork the FreshRSS repository. +3. Get your repository `git clone -b dev git@github.com:your_username/FreshRSS.git && cd FreshRSS` +4. It's better to create a dedicated branch to fix your bug: the name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783). +5. Once you'll have finished, commit your work, push your branch upstream (`git push --set-upstream origin your_branch_name`) and do a [pull request](https://github.com/FreshRSS/FreshRSS/compare) on the **dev branch**. +6. Wait and see. + +If you have to write code, please follow [our coding style recommendations](http://doc2.freshrss.org/en/Developer_documentation/First_steps/Coding_style). + +**Tip:** if you are searching for bugs easy to fix, have a look at the « [New comers](https://github.com/FreshRSS/FreshRSS/labels/New%20comers) » ticket label. + +## Submit an idea + +You have great ideas, yes! Don't be shy and open [a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new) on our bug tracker to ask if we can implement it. The greatest ideas often come from the shyest suggestions! + +If your idea is nice, we'll have a look at it. + +TODO: complete + +## Contribute to internationalization (i18n) + +If you want to improve internationalization, please open a new ticket first and follow indications from « Fix a bug » section. + +TODO: finish + +We are working on a better way to handle internationalization. + +## Contribute to documentation + +The documentation needs a lot of improvements in order to be more useful to new contributors and we are working on it. If you want to give some help, meet us on [the dedicated repository](https://github.com/FreshRSS/documentation)! + +TODO: finish From 4bb8548eccf22b40c25352fe27c66f1f8039ebcd Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sun, 8 Feb 2015 20:51:41 -0500 Subject: [PATCH 04/90] 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 +++++++++++++++++++ tests/app/Models/ContextTest.php | 241 +++++++++++++++++++++++++++++++ 2 files changed, 390 insertions(+) create mode 100644 tests/app/Models/ContextTest.php 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(), + ); + } + } diff --git a/tests/app/Models/ContextTest.php b/tests/app/Models/ContextTest.php new file mode 100644 index 000000000..c5da6f667 --- /dev/null +++ b/tests/app/Models/ContextTest.php @@ -0,0 +1,241 @@ +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'), + ); + } + +} From f30ded2f863ed4a33ae8194d6cf00eaa877c9b6a Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 11 Feb 2015 20:41:00 -0500 Subject: [PATCH 05/90] 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 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', + ), + ); + } + +} From 9cee5c1a17947d3f3d10554844b7089e28cf8500 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 11 Feb 2015 21:52:10 -0500 Subject: [PATCH 06/90] Remove code generated by netbeans --- app/Models/Search.php | 2 -- 1 file changed, 2 deletions(-) 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 { From 9f83aa5fe79618be98cb027bb1070f5a11c51723 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Thu, 12 Feb 2015 21:05:33 -0500 Subject: [PATCH 07/90] 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(-) 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); From 964e67d4a4914008b0f7bc941d77215b2038012c Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Thu, 12 Feb 2015 21:08:37 -0500 Subject: [PATCH 08/90] Add a test to verify if the search input is stored correctly --- tests/app/Models/SearchTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php index 6ddfc0370..20ea09433 100644 --- a/tests/app/Models/SearchTest.php +++ b/tests/app/Models/SearchTest.php @@ -230,6 +230,7 @@ class SearchTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($max_pubdate_value, $search->getMaxPubdate()); $this->assertEquals($tags_value, $search->getTags()); $this->assertEquals($search_value, $search->getSearch()); + $this->assertEquals($input, $search->getRawInput()); } public function provideMultipleSearch() { From b8fd3caf8306e8616fcb2f2c0add95b74c2ec024 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 14 Feb 2015 11:00:26 -0500 Subject: [PATCH 09/90] 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(-) 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'); From 6e7bdb1709deaa0a1e6e5f4de10139a1e653e500 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sun, 15 Feb 2015 18:18:28 -0500 Subject: [PATCH 10/90] Add a cleaning process in the auto-remove feature. Before, when the status change was too quick, sometimes, articles weren't removed from the view. Now they are removed by adding a statement that grabs past articles with specific properties and remove them. See #738 --- p/scripts/main.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/p/scripts/main.js b/p/scripts/main.js index 1be75bb12..b58d4de81 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -238,12 +238,7 @@ function toggleContent(new_active, old_active) { old_active.removeClass("active current"); new_active.addClass("current"); if (context['auto_remove_article'] && !old_active.hasClass('not_read')) { - var p = old_active.prev(); - var n = old_active.next(); - if (p.hasClass('day') && n.hasClass('day')) { - p.remove(); - } - old_active.remove(); + auto_remove(old_active); } } else { new_active.toggleClass('active'); @@ -258,7 +253,7 @@ function toggleContent(new_active, old_active) { if (context['sticky_post']) { var prev_article = new_active.prevAll('.flux'), - new_pos = new_active.position().top, + new_pos = new_active.position().top, old_scroll = $(box_to_move).scrollTop(); if (prev_article.length > 0 && new_pos - prev_article.position().top <= 150) { @@ -289,6 +284,16 @@ function toggleContent(new_active, old_active) { } } +function auto_remove(element) { + var p = element.prev(); + var n = element.next(); + if (p.hasClass('day') && n.hasClass('day')) { + p.remove(); + } + element.remove(); + $('#stream > .flux:not(.not_read):not(.active)').remove(); +} + function prev_entry() { var old_active = $(".flux.current"), new_active = old_active.length === 0 ? $(".flux:last") : old_active.prevAll(".flux:first"); @@ -683,7 +688,7 @@ function init_stream(divStream) { } var old_active = $(".flux.current"), new_active = $(this).parent(); - isCollapsed = true; + isCollapsed = true; if (e.target.tagName.toUpperCase() === 'A') { //Leave real links alone if (context['auto_mark_article']) { mark_read(new_active, true); @@ -696,12 +701,7 @@ function init_stream(divStream) { divStream.on('click', '.flux a.read', function () { var active = $(this).parents(".flux"); if (context['auto_remove_article'] && active.hasClass('not_read')) { - var p = active.prev(); - var n = active.next(); - if (p.hasClass('day') && n.hasClass('day')) { - p.remove(); - } - active.remove(); + auto_remove(active); } mark_read(active, false); return false; @@ -882,8 +882,8 @@ function notifs_html5_show(nb) { if (context['html5_notif_timeout'] !== 0){ setTimeout(function() { - notification.close(); - }, context['html5_notif_timeout'] * 1000); + notification.close(); + }, context['html5_notif_timeout'] * 1000); } } @@ -899,7 +899,7 @@ function init_notifs_html5() { function refreshUnreads() { $.getJSON('./?c=javascript&a=nbUnreadsPerFeed').done(function (data) { var isAll = $('.category.all.active').length > 0, - new_articles = false; + new_articles = false; $.each(data, function(feed_id, nbUnreads) { feed_id = 'f_' + feed_id; @@ -1195,7 +1195,7 @@ function faviconNbUnread(n) { function init_slider_observers() { var slider = $('#slider'), - closer = $('#close-slider'); + closer = $('#close-slider'); if (slider.length < 1) { return; } From ee56f3607df3f6f31082b1f86d89e9b705dc93c8 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Tue, 17 Feb 2015 14:45:26 +0100 Subject: [PATCH 11/90] Improve CONTRIBUTING file - remove TODO - simplify info about "fix a bug" (get from https://github.com/github/gitignore README) - add few info See https://github.com/FreshRSS/FreshRSS/issues/783 --- CONTRIBUTING.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb1610a1f..a80664f67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,11 +23,9 @@ If you have to create a new ticket, try to apply the following advices: Did you want to fix a bug? To keep a great coordination between collaborators, you will have to follow these indications: 1. Be sure the bug is associated to a ticket and say you work on it. -2. Fork the FreshRSS repository. -3. Get your repository `git clone -b dev git@github.com:your_username/FreshRSS.git && cd FreshRSS` -4. It's better to create a dedicated branch to fix your bug: the name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783). -5. Once you'll have finished, commit your work, push your branch upstream (`git push --set-upstream origin your_branch_name`) and do a [pull request](https://github.com/FreshRSS/FreshRSS/compare) on the **dev branch**. -6. Wait and see. +2. [Fork this project repository](https://help.github.com/articles/fork-a-repo/). +3. [Create a new branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/). The name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783). +4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **dev branch**. If you have to write code, please follow [our coding style recommendations](http://doc2.freshrss.org/en/Developer_documentation/First_steps/Coding_style). @@ -39,18 +37,16 @@ You have great ideas, yes! Don't be shy and open [a new ticket](https://github.c If your idea is nice, we'll have a look at it. -TODO: complete - ## Contribute to internationalization (i18n) If you want to improve internationalization, please open a new ticket first and follow indications from « Fix a bug » section. -TODO: finish +Translations are present in the subdirectories of `./app/i18n/`. -We are working on a better way to handle internationalization. +We are working on a better way to handle internationalization but don't hesitate to suggest any idea! ## Contribute to documentation The documentation needs a lot of improvements in order to be more useful to new contributors and we are working on it. If you want to give some help, meet us on [the dedicated repository](https://github.com/FreshRSS/documentation)! -TODO: finish +The best is to open tickets to ask your questions or to do any suggestions. From 3aec3e717173713d6a817110fb61b3702c929b1d Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Tue, 17 Feb 2015 14:52:18 +0100 Subject: [PATCH 12/90] Improve CONTRIBUTING.md file Add a section about mailing lists See https://github.com/FreshRSS/FreshRSS/issues/783 --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a80664f67..3f72c5cd7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,12 @@ # How to contribute to FreshRSS? +## Join us on the mailing lists + +Do you want to ask us some questions? Do you want to discuss with us? Don't hesitate to subscribe to our mailing lists! + +- The first mailing is destined to generic information, it should be adapted to users. [Join mailing@freshrss.org](https://freshrss.org/mailman/listinfo/mailing). +- The second mailing is mainly for developers. [Join dev@freshrss.org](https://freshrss.org/mailman/listinfo/dev) + ## Report a bug You found a bug? Don't panic, here are some steps to report it easily: From 58bf976f6923b5ad9c6e8e57a0daa06ec05462f4 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Tue, 17 Feb 2015 21:05:28 -0500 Subject: [PATCH 13/90] Change translation I change the translation for the 3 languages. German has to be checked since I used Google translate to get the translation. --- app/i18n/de/gen.php | 2 +- app/i18n/en/gen.php | 2 +- app/i18n/fr/gen.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index 3170b29c2..1b30a5be4 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -156,7 +156,7 @@ return array( 'damn' => 'Verdammt!', 'default_category' => 'Unkategorisiert', 'no' => 'Nein', - 'not_applicable' => 'N/A', + 'not_applicable' => 'Nicht verfügbar', 'ok' => 'OK!', 'or' => 'oder', 'yes' => 'Ja', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 420e73f36..fdca01a43 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -156,7 +156,7 @@ return array( 'damn' => 'Damn!', 'default_category' => 'Uncategorized', 'no' => 'No', - 'not_applicable' => 'N/A', + 'not_applicable' => 'Not available', 'ok' => 'Ok!', 'or' => 'or', 'yes' => 'Yes', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index ae946ce0c..48fc4a7e9 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -156,7 +156,7 @@ return array( 'damn' => 'Arf !', 'default_category' => 'Sans catégorie', 'no' => 'Non', - 'not_applicable' => 'N/A', + 'not_applicable' => 'Non disponible', 'ok' => 'Ok !', 'or' => 'ou', 'yes' => 'Oui', From e7a35161e29b3319ff3c0956c3de671735cd6d9a Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 18 Feb 2015 16:05:55 +0100 Subject: [PATCH 14/90] Update CONTRIBUTING file Remove unnecessary sentences in "Contribute to documentation" See https://github.com/FreshRSS/FreshRSS/issues/783 --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f72c5cd7..133813711 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,5 +55,3 @@ We are working on a better way to handle internationalization but don't hesitate ## Contribute to documentation The documentation needs a lot of improvements in order to be more useful to new contributors and we are working on it. If you want to give some help, meet us on [the dedicated repository](https://github.com/FreshRSS/documentation)! - -The best is to open tickets to ask your questions or to do any suggestions. From f5028d30d047ae50ac2d89a9a66266de086ac718 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 21 Feb 2015 07:53:45 -0500 Subject: [PATCH 15/90] 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(+) 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: [PATCH 16/90] 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(-) 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, From 50b6a02578c29c780e6fe36af712bdc7cfe81d3d Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 21 Feb 2015 09:42:06 -0500 Subject: [PATCH 17/90] 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(-) 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()); From 74c020edc846d0afdecb05ae1e1f30a516dee002 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 21 Feb 2015 09:44:02 -0500 Subject: [PATCH 18/90] 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(+) 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; From e897afa7ccb2c625705bce25c003d9cf37179227 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sun, 22 Feb 2015 17:38:33 -0500 Subject: [PATCH 19/90] Change test to verify if there is a filter --- app/Models/EntryDAO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()}%"; From fe24636e0416df4bb3507a0ac8cfe7e27d5b4c90 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Mon, 2 Mar 2015 20:27:15 -0500 Subject: [PATCH 20/90] Add missing white space --- app/Models/EntryDAO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()) { From f3f8d73dda3c9882f383e721eb3cc47be5a6c706 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Tue, 3 Mar 2015 22:35:22 -0500 Subject: [PATCH 21/90] Fix API to use the search object Since the internal of the listWhere method was changed, the API wasn't working. It was still calling the method with the old parameters. I didn't test it but now, it should work. --- p/api/greader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p/api/greader.php b/p/api/greader.php index ab1a02244..060aa45ee 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -371,7 +371,7 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex } $entryDAO = FreshRSS_Factory::createEntryDao(); - $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, '', $start_time); + $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_Search(''), $start_time); $items = array(); foreach ($entries as $entry) { From d1c9378d338027e39174fecb5f7a047218ad2113 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 4 Mar 2015 23:04:12 -0500 Subject: [PATCH 22/90] Fix entry DAO query usage I did not fix the call in the previous commit. I hope this one is the last change needed. We definitely need a templating engine so we could use the same controller to output different things. This will remove code duplication between the api and the web interface. It will allows us to build other type of api, and also refactor the rss feed as a different view of the same dataset. --- p/api/greader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p/api/greader.php b/p/api/greader.php index 060aa45ee..4554a3f9c 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -465,7 +465,7 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude } $entryDAO = FreshRSS_Factory::createEntryDao(); - $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, '', '', $start_time); + $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, '', new FreshRSS_Search(''), $start_time); $itemRefs = array(); foreach ($ids as $id) { From aacd1ffd4052b04c724c2783b3ee2845ca4ada35 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 4 Mar 2015 23:32:20 -0500 Subject: [PATCH 23/90] Refactor exceptions I removed unnecessary constructors and unnecessary inheritance --- app/Exceptions/BadUrlException.php | 5 ++++- app/Exceptions/ContextException.php | 6 ++---- app/Exceptions/EntriesGetterException.php | 6 ++---- app/Exceptions/FeedException.php | 7 +++---- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/Exceptions/BadUrlException.php b/app/Exceptions/BadUrlException.php index 59574e1e5..406efb9b3 100644 --- a/app/Exceptions/BadUrlException.php +++ b/app/Exceptions/BadUrlException.php @@ -1,6 +1,9 @@ Date: Thu, 5 Mar 2015 06:51:16 -0500 Subject: [PATCH 24/90] Revert inheritance As I do not have the time at the moment to verify the change of inheritance, I changed it back to the original. --- app/Exceptions/BadUrlException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Exceptions/BadUrlException.php b/app/Exceptions/BadUrlException.php index 406efb9b3..d2509e4ba 100644 --- a/app/Exceptions/BadUrlException.php +++ b/app/Exceptions/BadUrlException.php @@ -1,6 +1,6 @@ Date: Sun, 1 Mar 2015 09:18:06 -0500 Subject: [PATCH 25/90] 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 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()); + } + +} From 96d5d9d034daadb2357f27b3763854b647f2086c Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Thu, 5 Mar 2015 06:45:00 -0500 Subject: [PATCH 26/90] 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(-) 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() { From 24f6c1eabb4cea941e40307c2f732c0ca384ffd2 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Thu, 5 Mar 2015 06:47:13 -0500 Subject: [PATCH 27/90] Fix spacing --- app/Models/CategoryDAO.php | 2 +- app/Models/EntryDAO.php | 2 +- app/Models/FeedDAO.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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); From dd4fb519be801254a8aa9baedb2a7f87dd608f2b Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 14 Mar 2015 09:43:08 -0400 Subject: [PATCH 28/90] Add an unsaved changes alert on config pages Before, you could leave a configuration page without knowing if you saved your changes or not. Now, there is an alert poping up if you have unsaved changes. It will ask you if you want to stay on the page and save your changes or leave the page and loose your changes. See #739 --- app/views/auth/index.phtml | 12 +++++----- app/views/configure/archiving.phtml | 6 ++--- app/views/configure/display.phtml | 28 ++++++++++++------------ app/views/configure/queries.phtml | 1 + app/views/configure/reading.phtml | 34 ++++++++++++++--------------- app/views/configure/sharing.phtml | 4 ++-- app/views/configure/shortcut.phtml | 28 ++++++++++++------------ p/scripts/main.js | 28 ++++++++++++++++++++++++ 8 files changed, 85 insertions(+), 56 deletions(-) diff --git a/app/views/auth/index.phtml b/app/views/auth/index.phtml index f7a862ac9..8e4df8c2c 100644 --- a/app/views/auth/index.phtml +++ b/app/views/auth/index.phtml @@ -9,7 +9,7 @@
- auth_type, array('form', 'persona', 'http_auth', 'none'))) { ?> @@ -25,7 +25,7 @@
@@ -35,7 +35,7 @@
@@ -45,7 +45,7 @@
@@ -58,7 +58,7 @@ token; ?>
/> + echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation=""/> array('output' => 'rss', 'token' => $token)), 'html', true); ?>
@@ -69,7 +69,7 @@
diff --git a/app/views/configure/archiving.phtml b/app/views/configure/archiving.phtml index 875463137..52ee98a48 100644 --- a/app/views/configure/archiving.phtml +++ b/app/views/configure/archiving.phtml @@ -10,14 +10,14 @@
- +  
- ttl_default; ?>"> '20min', 1500 => '25min', 1800 => '30min', 2700 => '45min', 3600 => '1h', 5400 => '1.5h', 7200 => '2h', 10800 => '3h', 14400 => '4h', 18800 => '5h', 21600 => '6h', 25200 => '7h', 28800 => '8h', diff --git a/app/views/configure/display.phtml b/app/views/configure/display.phtml index 02249bc55..91b0b8189 100644 --- a/app/views/configure/display.phtml +++ b/app/views/configure/display.phtml @@ -9,7 +9,7 @@
- @@ -24,7 +24,7 @@
    themes); $i = 1; ?> themes as $theme) { ?> - theme === $theme['id']) {echo "checked";}?> value=""/> + theme === $theme['id']) {echo "checked";}?> value="" data-leave-validation="theme === $theme['id']) ? 1 : 0; ?>"/>
  • @@ -53,7 +53,7 @@
    - @@ -87,20 +87,20 @@ - topline_read ? ' checked="checked"' : ''; ?> /> - topline_favorite ? ' checked="checked"' : ''; ?> /> + topline_read ? ' checked="checked"' : ''; ?> data-leave-validation="topline_read; ?>"/> + topline_favorite ? ' checked="checked"' : ''; ?> data-leave-validation="topline_favorite; ?>"/> - topline_date ? ' checked="checked"' : ''; ?> /> - topline_link ? ' checked="checked"' : ''; ?> /> + topline_date ? ' checked="checked"' : ''; ?> data-leave-validation="topline_date; ?>"/> + topline_link ? ' checked="checked"' : ''; ?> data-leave-validation="topline_link; ?>"/> - bottomline_read ? ' checked="checked"' : ''; ?> /> - bottomline_favorite ? ' checked="checked"' : ''; ?> /> - bottomline_sharing ? ' checked="checked"' : ''; ?> /> - bottomline_tags ? ' checked="checked"' : ''; ?> /> - bottomline_date ? ' checked="checked"' : ''; ?> /> - bottomline_link ? ' checked="checked"' : ''; ?> /> + bottomline_read ? ' checked="checked"' : ''; ?> data-leave-validation="bottomline_read; ?>"/> + bottomline_favorite ? ' checked="checked"' : ''; ?> data-leave-validation="bottomline_favorite; ?>"/> + bottomline_sharing ? ' checked="checked"' : ''; ?> data-leave-validation="bottomline_sharing; ?>"/> + bottomline_tags ? ' checked="checked"' : ''; ?> data-leave-validation="bottomline_tags; ?>"/> + bottomline_date ? ' checked="checked"' : ''; ?> data-leave-validation="bottomline_date; ?>"/> + bottomline_link ? ' checked="checked"' : ''; ?> data-leave-validation="bottomline_link; ?>"/>
    @@ -109,7 +109,7 @@
    - +
    diff --git a/app/views/configure/queries.phtml b/app/views/configure/queries.phtml index 69efcf365..50df4cfea 100644 --- a/app/views/configure/queries.phtml +++ b/app/views/configure/queries.phtml @@ -25,6 +25,7 @@ id="queries__name" name="queries[][name]" value="getName(); ?>" + data-leave-validation="getName(); ?>" /> diff --git a/app/views/configure/reading.phtml b/app/views/configure/reading.phtml index 636671f14..8b123afa8 100644 --- a/app/views/configure/reading.phtml +++ b/app/views/configure/reading.phtml @@ -9,7 +9,7 @@
    - +
    @@ -17,7 +17,7 @@
    - @@ -27,7 +27,7 @@
    - @@ -38,7 +38,7 @@
    - @@ -49,7 +49,7 @@
    @@ -58,7 +58,7 @@
    @@ -68,7 +68,7 @@
    @@ -78,7 +78,7 @@
    @@ -88,7 +88,7 @@
    @@ -98,7 +98,7 @@
    @@ -108,7 +108,7 @@
    @@ -118,7 +118,7 @@
    @@ -129,19 +129,19 @@
    @@ -151,7 +151,7 @@
    diff --git a/app/views/configure/sharing.phtml b/app/views/configure/sharing.phtml index deb1ed6b7..7bf435777 100644 --- a/app/views/configure/sharing.phtml +++ b/app/views/configure/sharing.phtml @@ -28,9 +28,9 @@
    - + formType() === 'advanced') { ?> - + diff --git a/app/views/configure/shortcut.phtml b/app/views/configure/shortcut.phtml index f68091af9..264a5f805 100644 --- a/app/views/configure/shortcut.phtml +++ b/app/views/configure/shortcut.phtml @@ -23,28 +23,28 @@
    - +
    - +
    - +
    - +
    @@ -53,7 +53,7 @@
    - +
    @@ -61,21 +61,21 @@
    - +
    - +
    - +
    @@ -83,7 +83,7 @@
    - +
    @@ -92,21 +92,21 @@
    - +
    - +
    - +
    @@ -114,14 +114,14 @@
    - +
    - +
    diff --git a/p/scripts/main.js b/p/scripts/main.js index b7c143cc1..eaf6067f7 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -1229,6 +1229,33 @@ function init_slider_observers() { }); } +function init_configuration_alert() { + $(window).on('beforeunload', function(e){ + if (e.originalEvent.explicitOriginalTarget.type === 'submit') { + // we don't want an alert when submitting the form with the submit button + return; + } + if ($(e.originalEvent.explicitOriginalTarget).attr('data-leave-validation') !== undefined) { + // we don't want an alert when submitting the form by pressing the enter key + return; + } + var fields = $("[data-leave-validation]"); + for (var i = 0; i < fields.length; i++) { + if ($(fields[i]).attr('type') === 'checkbox' || $(fields[i]).attr('type') === 'radio') { + // The use of != is done on purpose to check boolean against integer + if ($(fields[i]).is(':checked') != $(fields[i]).attr('data-leave-validation')) { + return false; + } + } else { + if ($(fields[i]).attr('data-leave-validation') !== $(fields[i]).val()) { + return false; + } + } + } + return; + }); +} + function init_all() { if (!(window.$ && window.context)) { if (window.console) { @@ -1260,6 +1287,7 @@ function init_all() { init_password_observers(); init_stats_observers(); init_slider_observers(); + init_configuration_alert(); } if (window.console) { From ec611d7d543e27fe8c794cd98177d00cf636c58f Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Mon, 16 Mar 2015 21:37:28 +0100 Subject: [PATCH 29/90] SimplePie: decode special chars for MAYBE_HTML https://github.com/FreshRSS/FreshRSS/issues/754 Needs to check with many feeds to see if this does not introduce incompatibilities with some valid feeds. --- lib/SimplePie/SimplePie/Sanitize.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/SimplePie/SimplePie/Sanitize.php b/lib/SimplePie/SimplePie/Sanitize.php index 168a5e2e8..3da7239be 100644 --- a/lib/SimplePie/SimplePie/Sanitize.php +++ b/lib/SimplePie/SimplePie/Sanitize.php @@ -249,6 +249,11 @@ class SimplePie_Sanitize { if ($type & SIMPLEPIE_CONSTRUCT_MAYBE_HTML) { + if (preg_match('/&#(x[0-9a-fA-F]+|[0-9]+);/', $data)) //FreshRSS + { + $data = htmlspecialchars_decode($data, ENT_QUOTES); //FreshRSS + //syslog(LOG_DEBUG, 'SimplePie sanitize MAYBE_HTML htmlspecialchars_decode: ' . $data); //FreshRSS + } if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>)/', $data)) { $type |= SIMPLEPIE_CONSTRUCT_HTML; From 0e08b5ba56b2117e0f3d9bea6ae725295e8700d5 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 21 Mar 2015 15:13:08 +0100 Subject: [PATCH 30/90] htmlspecialchars_decode for all SIMPLEPIE_CONSTRUCT_MAYBE_HTML https://github.com/FreshRSS/FreshRSS/issues/754 --- lib/SimplePie/SimplePie/Sanitize.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/SimplePie/SimplePie/Sanitize.php b/lib/SimplePie/SimplePie/Sanitize.php index 3da7239be..e7c9f925f 100644 --- a/lib/SimplePie/SimplePie/Sanitize.php +++ b/lib/SimplePie/SimplePie/Sanitize.php @@ -249,11 +249,7 @@ class SimplePie_Sanitize { if ($type & SIMPLEPIE_CONSTRUCT_MAYBE_HTML) { - if (preg_match('/&#(x[0-9a-fA-F]+|[0-9]+);/', $data)) //FreshRSS - { - $data = htmlspecialchars_decode($data, ENT_QUOTES); //FreshRSS - //syslog(LOG_DEBUG, 'SimplePie sanitize MAYBE_HTML htmlspecialchars_decode: ' . $data); //FreshRSS - } + $data = htmlspecialchars_decode($data, ENT_QUOTES); //FreshRSS if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>)/', $data)) { $type |= SIMPLEPIE_CONSTRUCT_HTML; From 1a35e2271d3b9383e882371d37d5fef16abd745d Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 21 Mar 2015 18:20:36 +0100 Subject: [PATCH 31/90] 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(-) 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']); From ad9fe52f5a76faf58d13fcf7bde8f58e85abe82b Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 22 Mar 2015 22:54:29 +0100 Subject: [PATCH 32/90] 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(-) 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); -} From 1c38e646c3223571988b33e6a0d481426e3b6931 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 22 Mar 2015 23:53:59 +0100 Subject: [PATCH 33/90] SimplePie seems to only supports HTTP schemes Can simplify the regex (faster because anchored) for cleaning URLs based on the fact that we only have to deal with HTTP or HTTPS --- lib/SimplePie/SimplePie/Misc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php index de50d37b8..90f01f737 100644 --- a/lib/SimplePie/SimplePie/Misc.php +++ b/lib/SimplePie/SimplePie/Misc.php @@ -2248,7 +2248,7 @@ function embed_wmedia(width, height, link) { */ function url_remove_credentials($url) //FreshRSS { - return preg_replace('#(?<=//)[^/:@]+:[^/:@]+@#', '', $url); + return preg_replace('#(?<=^https?://)[^/:@]+:[^/:@]+@#', '', $url); } } From 0ed213d97f566d7f46747d28a318cf58cdb5d699 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 22 Mar 2015 23:56:47 +0100 Subject: [PATCH 34/90] Revert "SimplePie seems to only supports HTTP schemes" This reverts commit 1c38e646c3223571988b33e6a0d481426e3b6931. --- lib/SimplePie/SimplePie/Misc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php index 90f01f737..de50d37b8 100644 --- a/lib/SimplePie/SimplePie/Misc.php +++ b/lib/SimplePie/SimplePie/Misc.php @@ -2248,7 +2248,7 @@ function embed_wmedia(width, height, link) { */ function url_remove_credentials($url) //FreshRSS { - return preg_replace('#(?<=^https?://)[^/:@]+:[^/:@]+@#', '', $url); + return preg_replace('#(?<=//)[^/:@]+:[^/:@]+@#', '', $url); } } From 7735471140e1c6087a177192a09c7fb849decd77 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Mon, 23 Mar 2015 21:58:37 +0100 Subject: [PATCH 35/90] SimplePie faster regex for sanitizing URLs Can simplify the regex (faster because anchored) for cleaning URLs based on the fact that we only have to deal with HTTP or HTTPS https://github.com/FreshRSS/FreshRSS/pull/715 --- lib/SimplePie/SimplePie/Misc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php index de50d37b8..91549f93a 100644 --- a/lib/SimplePie/SimplePie/Misc.php +++ b/lib/SimplePie/SimplePie/Misc.php @@ -2248,7 +2248,7 @@ function embed_wmedia(width, height, link) { */ function url_remove_credentials($url) //FreshRSS { - return preg_replace('#(?<=//)[^/:@]+:[^/:@]+@#', '', $url); + return preg_replace('#^(https?://)[^/:@]+:[^/:@]+@#i', '$1', $url); } } From 2bfc4dbf8b19cd2b892526ef65a966ff3bfef718 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 24 Mar 2015 21:58:00 +0100 Subject: [PATCH 36/90] SimplePie forgot static keyword https://github.com/FreshRSS/FreshRSS/issues/711 --- lib/SimplePie/SimplePie/Misc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php index 91549f93a..956a284cb 100644 --- a/lib/SimplePie/SimplePie/Misc.php +++ b/lib/SimplePie/SimplePie/Misc.php @@ -2246,7 +2246,7 @@ function embed_wmedia(width, height, link) { * @param $url the URL to sanitize. * @return the same URL without HTTP credentials. */ - function url_remove_credentials($url) //FreshRSS + public static function url_remove_credentials($url) //FreshRSS { return preg_replace('#^(https?://)[^/:@]+:[^/:@]+@#i', '$1', $url); } From 239a010ef23127429698b6be5f4a5453b0bfbad7 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 24 Mar 2015 22:45:27 +0100 Subject: [PATCH 37/90] Error when deleting a feed https://github.com/FreshRSS/FreshRSS/issues/816 --- app/Models/ConfigurationSetter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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(); + } } } From d7706b66f586b36e44e471b5c06526de258af8b8 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 24 Mar 2015 22:51:51 +0100 Subject: [PATCH 38/90] 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(-) 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(); } } From 4744a2a2d82f2b18df70b37f8cff5043dde12770 Mon Sep 17 00:00:00 2001 From: Tets Date: Thu, 26 Mar 2015 09:27:23 +0100 Subject: [PATCH 39/90] Added Czech translation --- app/i18n/cz/admin.php | 170 +++++++++++++++++++++++++++++++++++++++ app/i18n/cz/conf.php | 169 ++++++++++++++++++++++++++++++++++++++ app/i18n/cz/feedback.php | 110 +++++++++++++++++++++++++ app/i18n/cz/gen.php | 164 +++++++++++++++++++++++++++++++++++++ app/i18n/cz/index.php | 61 ++++++++++++++ app/i18n/cz/install.php | 107 ++++++++++++++++++++++++ app/i18n/cz/sub.php | 61 ++++++++++++++ 7 files changed, 842 insertions(+) create mode 100644 app/i18n/cz/admin.php create mode 100644 app/i18n/cz/conf.php create mode 100644 app/i18n/cz/feedback.php create mode 100644 app/i18n/cz/gen.php create mode 100644 app/i18n/cz/index.php create mode 100644 app/i18n/cz/install.php create mode 100644 app/i18n/cz/sub.php diff --git a/app/i18n/cz/admin.php b/app/i18n/cz/admin.php new file mode 100644 index 000000000..b9ef707cf --- /dev/null +++ b/app/i18n/cz/admin.php @@ -0,0 +1,170 @@ + array( + 'allow_anonymous' => 'Umožnit anonymně číst články výchozího uživatele (%s)', + 'allow_anonymous_refresh' => 'Umožnit anonymní obnovení článků', + 'api_enabled' => 'Povolit přístup k API (vyžadováno mobilními aplikacemi)', + 'form' => 'Webový formulář (tradiční, vyžaduje JavaScript)', + 'http' => 'HTTP (pro pokročilé uživatele s HTTPS)', + 'none' => 'Žádný (nebezpečné)', + 'persona' => 'Mozilla Persona (moderní, vyžaduje JavaScript)', + 'title' => 'Přihlášení', + 'title_reset' => 'Reset přihlášení', + 'token' => 'Authentizační token', + 'token_help' => 'Umožňuje přístup k RSS kanálu článků výchozího uživatele bez přihlášení:', + 'type' => 'Způsob přihlášení', + 'unsafe_autologin' => 'Povolit nebezpečné automatické přihlášení přes: ', + ), + 'check_install' => array( + 'cache' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/cache. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře cache jsou v pořádku.', + ), + 'categories' => array( + 'nok' => 'Tabulka kategorií je nastavena špatně.', + 'ok' => 'Tabulka kategorií je v pořádku.', + ), + 'connection' => array( + 'nok' => 'Nelze navázat spojení s databází.', + 'ok' => 'Připojení k databázi je v pořádku.', + ), + 'ctype' => array( + 'nok' => 'Nemáte požadovanou knihovnu pro ověřování znaků (php-ctype).', + 'ok' => 'Máte požadovanou knihovnu pro ověřování znaků (ctype).', + ), + 'curl' => array( + 'nok' => 'Nemáte cURL (balíček php5-curl).', + 'ok' => 'Máte rozšíření cURL.', + ), + 'data' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře data jsou v pořádku.', + ), + 'database' => 'Instalace databáze', + 'dom' => array( + 'nok' => 'Nemáte požadovanou knihovnu pro procházení DOM (balíček php-xml).', + 'ok' => 'Máte požadovanou knihovnu pro procházení DOM.', + ), + 'entries' => array( + 'nok' => 'Tabulka článků je nastavena špatně.', + 'ok' => 'Tabulka kategorií je v pořádku.', + ), + 'favicons' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/favicons. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře favicons jsou v pořádku.', + ), + 'feeds' => array( + 'nok' => 'Tabulka kanálů je nastavena špatně.', + 'ok' => 'Tabulka kanálů je v pořádku.', + ), + 'files' => 'Instalace souborů', + 'json' => array( + 'nok' => 'Nemáte JSON (balíček php5-json).', + 'ok' => 'Máte rozšíření JSON.', + ), + 'minz' => array( + 'nok' => 'Nemáte framework Minz.', + 'ok' => 'Máte framework Minz.', + ), + 'pcre' => array( + 'nok' => 'Nemáte požadovanou knihovnu pro regulární výrazy (php-pcre).', + 'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).', + ), + 'pdo' => array( + 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', + 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/persona. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře Mozilla Persona jsou v pořádku.', + ), + 'php' => array( + '_' => 'PHP instalace', + 'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.', + 'ok' => 'Vaše verze PHP je %s a je kompatibilní s FreshRSS.', + ), + 'tables' => array( + 'nok' => 'V databázi chybí jedna nevo více tabulek.', + 'ok' => 'V databázi jsou všechny tabulky.', + ), + 'title' => 'Kontrola instalace', + 'tokens' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/tokens. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře tokens jsou v pořádku.', + ), + 'users' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/users. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře users jsou v pořádku.', + ), + 'zip' => array( + 'nok' => 'Nemáte rozšíření ZIP (balíček php5-zip).', + 'ok' => 'Máte rozšíření ZIP.', + ), + ), + 'extensions' => array( + 'disabled' => 'Vypnuto', + 'empty_list' => 'Není naistalováno žádné rozšíření', + 'enabled' => 'Zapnuto', + 'no_configure_view' => 'Toto rozšíření nemá žádné možnosti nastavení.', + 'system' => array( + '_' => 'Systémová rozšíření', + 'no_rights' => 'Systémová rozšíření (na ně nemáte oprávnění)', + ), + 'title' => 'Rozšíření', + 'user' => 'Uživatelská rozšíření', + ), + 'stats' => array( + '_' => 'Statistika', + 'all_feeds' => 'Všechny kanály', + 'category' => 'Kategorie', + 'entry_count' => 'Počet článků', + 'entry_per_category' => 'Článků na kategorii', + 'entry_per_day' => 'Článků za den (posledních 30 dní)', + 'entry_per_day_of_week' => 'Za den v týdnu (průměr: %.2f zprávy)', + 'entry_per_hour' => 'Za hodinu (průměr: %.2f zprávy)', + 'entry_per_month' => 'Za měsíc (průměr: %.2f zprávy)', + 'entry_repartition' => 'Rozdělení článků', + 'feed' => 'Kanál', + 'feed_per_category' => 'Článků na kategorii', + 'idle' => 'Neaktivní kanály', + 'main' => 'Přehled', + 'main_stream' => 'Všechny kanály', + 'menu' => array( + 'idle' => 'Neaktivní kanály', + 'main' => 'Přehled', + 'repartition' => 'Rozdělení článků', + ), + 'no_idle' => 'Žádné neaktivní kanály!', + 'number_entries' => '%d článků', + 'percent_of_total' => '%% ze všech', + 'repartition' => 'Rozdělení článků', + 'status_favorites' => 'Oblíbené', + 'status_read' => 'Přečtené', + 'status_total' => 'Celkem', + 'status_unread' => 'Nepřečtené', + 'title' => 'Statistika', + 'top_feed' => 'Top ten kanálů', + ), + 'update' => array( + '_' => 'Aktualizace systému', + 'apply' => 'Použít', + 'check' => 'Zkontrolovat aktualizace', + 'current_version' => 'Vaše instalace FreshRSS je verze %s.', + 'last' => 'Poslední kontrola: %s', + 'none' => 'Žádné nové aktualizace', + 'title' => 'Aktualizovat systém', + ), + 'user' => array( + 'articles_and_size' => '%s článků (%s)', + 'create' => 'Vytvořit nového uživatele', + 'email_persona' => 'Email pro přihlášení
    (pro Mozilla Persona)', + 'language' => 'Jazyk', + 'password_form' => 'Heslo
    (pro přihlášení webovým formulářem)', + 'password_format' => 'Alespoň 7 znaků', + 'title' => 'Správa uživatelů', + 'user_list' => 'Seznam uživatelů', + 'username' => 'Přihlašovací jméno', + 'users' => 'Uživatelé', + ), +); diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php new file mode 100644 index 000000000..29fb1e4d4 --- /dev/null +++ b/app/i18n/cz/conf.php @@ -0,0 +1,169 @@ + array( + '_' => 'Archivace', + 'advanced' => 'Pokročilé', + 'delete_after' => 'Smazat články starší než', + 'help' => 'Více možností je dostupných v nastavení jednotlivých kanálů', + 'keep_history_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu', + 'optimize' => 'Optimalizovat databázi', + 'optimize_help' => 'Občasná údržba zmenší velikost databáze', + 'purge_now' => 'Vyčistit nyní', + 'title' => 'Archivace', + 'ttl' => 'Neaktualizovat častěji než', + ), + 'display' => array( + '_' => 'Zobrazení', + 'icon' => array( + 'bottom_line' => 'Spodní řádek', + 'entry' => 'Ikony článků', + 'publication_date' => 'Datum vydání', + 'related_tags' => 'Související tagy', + 'sharing' => 'Sdílení', + 'top_line' => 'Horní řádek', + ), + 'language' => 'Jazyk', + 'notif_html5' => array( + 'seconds' => 'sekund (0 znamená žádný timeout)', + 'timeout' => 'Timeout HTML5 notifikací', + ), + 'theme' => 'Vzhled', + 'title' => 'Zobrazení', + 'width' => array( + 'content' => 'Šířka obsahu', + 'large' => 'Velká', + 'medium' => 'Střední', + 'no_limit' => 'Bez limitu', + 'thin' => 'Tenká', + ), + ), + 'query' => array( + '_' => 'Uživatelské dotazy', + 'deprecated' => 'Tento dotaz již není platný. Odkazovaná kategorie nebo kanál byly smazány.', + 'filter' => 'Filtr aplikován:', + 'get_all' => 'Zobrazit všechny články', + 'get_category' => 'Zobrazit "%s" kategorii', + 'get_favorite' => 'Zobrazit oblíbené články', + 'get_feed' => 'Zobrazit "%s" článkek', + 'no_filter' => 'Zrušit filtr', + 'none' => 'Ještě jste nevytvořil žádný uživatelský dotaz.', + 'number' => 'Dotaz n°%d', + 'order_asc' => 'Zobrazit nejdříve nejstarší články', + 'order_desc' => 'Zobrazit nejdříve nejnovější články', + 'search' => 'Hledat "%s"', + 'state_0' => 'Zobrazit všechny články', + 'state_1' => 'Zobrazit přečtené články', + 'state_2' => 'Zobrazit nepřečtené články', + 'state_3' => 'Zobrazit všechny články', + 'state_4' => 'Zobrazit oblíbené články', + 'state_5' => 'Zobrazit oblíbené přečtené články', + 'state_6' => 'Zobrazit oblíbené nepřečtené články', + 'state_7' => 'Zobrazit oblíbené články', + 'state_8' => 'Zobrazit všechny články vyjma oblíbených', + 'state_9' => 'Zobrazit všechny přečtené články vyjma oblíbených', + 'state_10' => 'Zobrazit všechny nepřečtené články vyjma oblíbených', + 'state_11' => 'Zobrazit všechny články vyjma oblíbených', + 'state_12' => 'Zobrazit všechny články', + 'state_13' => 'Zobrazit přečtené články', + 'state_14' => 'Zobrazit nepřečtené články', + 'state_15' => 'Zobrazit všechny články', + 'title' => 'Uživatelské dotazy', + ), + 'profile' => array( + '_' => 'Správa profilu', + 'email_persona' => 'Email pro přihlášení
    (pro
    Mozilla Persona)', + 'password_api' => 'Password API
    (tzn. pro mobilní aplikace)', + 'password_form' => 'Heslo
    (pro přihlášení webovým formulářem)', + 'password_format' => 'Alespoň 7 znaků', + 'title' => 'Profil', + ), + 'reading' => array( + '_' => 'Čtení', + 'after_onread' => 'Po “označit vše jako přečtené”,', + 'articles_per_page' => 'Počet článků na stranu', + 'auto_load_more' => 'Načítat další články dole na stránce', + 'auto_remove_article' => 'Po přečtení články schovat', + 'confirm_enabled' => 'Vyžadovat potvrzení pro akci “označit vše jako přečtené”', + 'display_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené', + 'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené', + 'hide_read_feeds' => 'Schovat kategorie a kanály s nulovým počtem nepřečtených článků (nefunguje s nastavením “Zobrazit všechny články”)', + 'img_with_lazyload' => 'Použít "lazy load" mód pro načítaní obrázků', + 'jump_next' => 'skočit na další nepřečtený (kanál nebo kategorii)', + 'number_divided_when_reader' => 'V režimu “Čtení” děleno dvěma.', + 'read' => array( + 'article_open_on_website' => 'když je otevřen původní web s článkem', + 'article_viewed' => 'během čtení článku', + 'scroll' => 'během skrolování', + 'upon_reception' => 'po načtení článku', + 'when' => 'Označit článek jako přečtený…', + ), + 'show' => array( + '_' => 'Počet zobrazených článků', + 'adaptive' => 'Vyberte zobrazení', + 'all_articles' => 'Zobrazit všechny články', + 'unread' => 'Zobrazit jen nepřečtené', + ), + 'sort' => array( + '_' => 'Řazení', + 'newer_first' => 'Nejdříve nejnovější', + 'older_first' => 'Nejdříve nejstarší', + ), + 'sticky_post' => 'Při otevření posunout článek nahoru', + 'title' => 'Čtení', + 'view' => array( + 'default' => 'Výchozí', + 'global' => 'Přehled', + 'normal' => 'Normální', + 'reader' => 'Čtení', + ), + ), + 'sharing' => array( + '_' => 'Sdílení', + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'Email', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'more_information' => 'Více informací', + 'print' => 'Tisk', + 'shaarli' => 'Shaarli', + 'share_name' => 'Jméno pro zobrazení', + 'share_url' => 'Jakou URL použít pro sdílení', + 'title' => 'Sdílení', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'shortcut' => array( + '_' => 'Zkratky', + 'article_action' => 'Články - akce', + 'auto_share' => 'Sdílet', + 'auto_share_help' => 'Je-li nastavena pouze jedna možnost sdílení, bude použita. Další možnosti jsou dostupné pomocí jejich čísla.', + 'close_dropdown' => 'Zavřít menu', + 'collapse_article' => 'Srolovat', + 'first_article' => 'Skočit na první článek', + 'focus_search' => 'Hledání', + 'help' => 'Zobrazit documentaci', + 'javascript' => 'Pro použití zkratek musí být povolen JavaScript', + 'last_article' => 'Skočit na poslední článek', + 'load_more' => 'Načíst více článků', + 'mark_read' => 'Označit jako přečtené', + 'mark_favorite' => 'Označit jako oblíbené', + 'navigation' => 'Navigace', + 'navigation_help' => 'Pomocí přepínače "Shift" fungují navigační zkratky v rámci kanálů.
    Pomocí přepínače "Alt" fungují v rámci kategorií.', + 'next_article' => 'Skočit na další článek', + 'other_action' => 'Ostatní akce', + 'previous_article' => 'Skočit na předchozí článek', + 'see_on_website' => 'Navštívit původní webovou stránku', + 'shift_for_all_read' => '+ shift označí vše jako přečtené', + 'title' => 'Zkratky', + 'user_filter' => 'Aplikovat uživatelské filtry', + 'user_filter_help' => 'Je-li nastaven pouze jeden filtr, bude použit. Další filtry jsou dostupné pomocí jejich čísla.', + ), + 'user' => array( + 'articles_and_size' => '%s článků (%s)', + 'current' => 'Aktuální uživatel', + 'is_admin' => 'je administrátor', + 'users' => 'Uživatelé', + ), +); diff --git a/app/i18n/cz/feedback.php b/app/i18n/cz/feedback.php new file mode 100644 index 000000000..52ff029ef --- /dev/null +++ b/app/i18n/cz/feedback.php @@ -0,0 +1,110 @@ + array( + 'optimization_complete' => 'Optimalizace dokončena', + ), + 'access' => array( + 'denied' => 'Nemáte oprávnění přistupovat na tuto stránku', + 'not_found' => 'Tato stránka neexistuje', + ), + 'auth' => array( + 'form' => array( + 'not_set' => 'Nastal problém s konfigurací přihlašovacího systému. Zkuste to prosím později.', + 'set' => 'Webové formulář je nyní výchozí přihlašovací systém.', + ), + 'login' => array( + 'invalid' => 'Login není platný', + 'success' => 'Jste přihlášen', + ), + 'logout' => array( + 'success' => 'Jste odhlášen', + ), + 'no_password_set' => 'Heslo administrátora nebylo nastaveno. Tato funkce není k dispozici.', + 'not_persona' => 'Resetovat lze pouze systém Persona.', + ), + 'conf' => array( + 'error' => 'Během ukládání nastavení došlo k chybě', + 'query_created' => 'Dotaz "%s" byl vytvořen.', + 'shortcuts_updated' => 'Zkratky byly aktualizovány', + 'updated' => 'Nastavení bylo aktualizováno', + ), + 'extensions' => array( + 'already_enabled' => '%s je již zapnut', + 'disable' => array( + 'ko' => '%s nelze vypnout. Pro více detailů zkontrolujte logy FressRSS.', + 'ok' => '%s je nyní vypnut', + ), + 'enable' => array( + 'ko' => '%s nelze zapnout. Pro více detailů zkontrolujte logy FressRSS.', + 'ok' => '%s je nyní zapnut', + ), + 'no_access' => 'Nemáte přístup k %s', + 'not_enabled' => '%s není ještě zapnut', + 'not_found' => '%s neexistuje', + ), + 'import_export' => array( + 'export_no_zip_extension' => 'Na serveru není naistalována podpora zip. Zkuste prosím exportovat soubory jeden po druhém.', + 'feeds_imported' => 'Vaše kanály byly naimportovány a nyní budou aktualizovány', + 'feeds_imported_with_errors' => 'Vaše kanály byly naimportovány, došlo ale k nějakým chybám', + 'file_cannot_be_uploaded' => 'Soubor nelze nahrát!', + 'no_zip_extension' => 'Na serveru není naistalována podpora zip.', + 'zip_error' => 'Během importu zip souboru došlo k chybě.', + ), + 'sub' => array( + 'actualize' => 'Aktualizovat', + 'category' => array( + 'created' => 'Kategorie %s byla vytvořena.', + 'deleted' => 'Kategorie byla smazána.', + 'emptied' => 'Kategorie byla vyprázdněna', + 'error' => 'Kategorii nelze aktualizovat', + 'name_exists' => 'Název kategorie již existuje.', + 'no_id' => 'Musíte upřesnit id kategorie.', + 'no_name' => 'Název kategorie nemůže být prázdný.', + 'not_delete_default' => 'Nelze smazat výchozí kategorii!', + 'not_exist' => 'Tato kategorie neexistuje!', + 'over_max' => 'Dosáhl jste maximálního počtu kategorií (%d)', + 'updated' => 'Kategorie byla aktualizována.', + ), + 'feed' => array( + 'actualized' => '%s bylo aktualizováno', + 'actualizeds' => 'RSS kanály byly aktualizovány', + 'added' => 'RSS kanál %s byl přidán', + 'already_subscribed' => 'Již jste přihlášen k odběru %s', + 'deleted' => 'Kanál byl smazán', + 'error' => 'Kanál nelze aktualizovat', + 'internal_problem' => 'RSS kanál nelze přidat. Pro detaily zkontrolujte logy FressRSS.', + 'invalid_url' => 'URL %s není platné', + 'marked_read' => 'Kanály byly označeny jako přečtené', + 'n_actualized' => '%d kanálů bylo aktualizováno', + 'n_entries_deleted' => '%d článků bylo smazáno', + 'no_refresh' => 'Nelze obnovit žádné kanály…', + 'not_added' => '%s nemůže být přidán', + 'over_max' => 'Dosáhl jste maximálního počtu kanálů (%d)', + 'updated' => 'Kanál byl aktualizován', + ), + 'purge_completed' => 'Vyprázdněno (smazáno %d článků)', + ), + 'update' => array( + 'can_apply' => 'FreshRSS bude nyní upgradováno na verzi %s.', + 'error' => 'Během upgrade došlo k chybě: %s', + 'file_is_nok' => 'Zkontrolujte oprávnění adresáře %s. HTTP server musí mít do tohoto adresáře práva zápisu', + 'finished' => 'Upgrade hotov!', + 'none' => 'Novější verze není k dispozici', + 'server_not_found' => 'Nelze nalézt server s instalačním souborem. [%s]', + ), + 'user' => array( + 'created' => array( + '_' => 'Uživatel %s byl vytvořen', + 'error' => 'Uživatele %s nelze vytvořit', + ), + 'deleted' => array( + '_' => 'Uživatel %s byl smazán', + 'error' => 'Uživatele %s nelze smazat', + ), + ), + 'profile' => array( + 'error' => 'Váš profil nelze změnit', + 'updated' => 'Váš profil byl změněn', + ), +); diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php new file mode 100644 index 000000000..7c333a9c8 --- /dev/null +++ b/app/i18n/cz/gen.php @@ -0,0 +1,164 @@ + array( + 'actualize' => 'Aktualizovat', + 'back_to_rss_feeds' => '← Zpět na seznam RSS kanálů', + 'cancel' => 'Zrušit', + 'create' => 'Vytvořit', + 'disable' => 'Zakázat', + 'empty' => 'Vyprázdnit', + 'enable' => 'Povolit', + 'export' => 'Export', + 'filter' => 'Filtrovat', + 'import' => 'Import', + 'manage' => 'Spravovat', + 'mark_read' => 'Označit jako přečtené', + 'mark_favorite' => 'Označit jako oblíbené', + 'remove' => 'Odstranit', + 'see_website' => 'Navštívit WWW stránku', + 'submit' => 'Odeslat', + 'truncate' => 'Smazat všechny články', + ), + 'auth' => array( + 'keep_logged_in' => 'Zapamatovat přihlášení (1 měsíc)', + 'login' => 'Login', + 'login_persona' => 'Přihlášení pomocí Persona', + 'login_persona_problem' => 'Problém s připojením k Persona?', + 'logout' => 'Odhlášení', + 'password' => 'Heslo', + 'reset' => 'Reset přihlášení', + 'username' => 'Uživatel', + 'username_admin' => 'Název administrátorského účtu', + 'will_reset' => 'Přihlašovací systém bude vyresetován: místo sytému Persona bude použito přihlášení formulářem.', + ), + 'date' => array( + 'Apr' => '\\D\\u\\b\\e\\n', + 'Aug' => '\\S\\r\\p\\e\\n', + 'Dec' => '\\P\\r\\o\\s\\i\\n\\e\\c', + 'Feb' => '\\Ú\\n\\o\\r', + 'Jan' => '\\L\\e\\d\\e\\n', + 'Jul' => '\\Č\\e\\r\\v\\e\\n\\e\\c', + 'Jun' => '\\Č\\e\\r\\v\\e\\n', + 'Mar' => '\\B\\ř\\e\\z\\e\\n', + 'May' => '\\K\\v\\ě\\t\\e\\n', + 'Nov' => '\\L\\i\\s\\t\\o\\p\\a\\d', + 'Oct' => '\\Ř\\í\\j\\e\\n', + 'Sep' => '\\Z\\á\\ř\\í', + 'apr' => 'dub', + 'april' => 'Dub', + 'aug' => 'srp', + 'august' => 'Srp', + 'before_yesterday' => 'Předevčírem', + 'dec' => 'pro', + 'december' => 'Pro', + 'feb' => 'úno', + 'february' => 'Úno', + 'format_date' => 'j\\. %s Y', + 'format_date_hour' => 'j\\. %s Y \\v H\\:i', + 'fri' => 'Pá', + 'jan' => 'led', + 'january' => 'Led', + 'jul' => 'čvn', + 'july' => 'Čvn', + 'jun' => 'čer', + 'june' => 'Čer', + 'last_3_month' => 'Minulé tři měsíce', + 'last_6_month' => 'Minulých šest měsíců', + 'last_month' => 'Minulý měsíc', + 'last_week' => 'Minulý týden', + 'last_year' => 'Minulý rok', + 'mar' => 'bře', + 'march' => 'Bře', + 'may' => 'Kvě', + 'mon' => 'Po', + 'month' => 'měsíce', + 'nov' => 'lis', + 'november' => 'Lis', + 'oct' => 'říj', + 'october' => 'Říj', + 'sat' => 'So', + 'sep' => 'zář', + 'september' => 'Zář', + 'sun' => 'Ne', + 'thu' => 'Čt', + 'today' => 'Dnes', + 'tue' => 'Út', + 'wed' => 'St', + 'yesterday' => 'Včera', + ), + 'freshrss' => array( + '_' => 'FreshRSS', + 'about' => 'O FreshRSS', + ), + 'js' => array( + 'category_empty' => 'Prázdná kategorie', + 'confirm_action' => 'Jste si jist, že chcete provést tuto akci? Změny nelze vrátit zpět!', + 'confirm_action_feed_cat' => 'Jste si jist, že chcete provést tuto akci? Přijdete o související oblíbené položky a uživatelské dotazy. Změny nelze vrátit zpět!', + 'feedback' => array( + 'body_new_articles' => 'Je \\d nových článků k přečtení v FreshRSS.', + 'request_failed' => 'Požadavek selhal, což může být způsobeno problémy s připojení k internetu.', + 'title_new_articles' => 'FreshRSS: nové články!', + ), + 'new_article' => 'Jsou k dispozici nové články, stránku obnovíte kliknutím zde.', + 'should_be_activated' => 'JavaScript musí být povolen', + ), + 'lang' => array( + 'de' => 'Deutsch', + 'en' => 'English', + 'fr' => 'Français', + 'cz' => 'Čeština', + ), + 'menu' => array( + 'about' => 'O aplikaci', + 'admin' => 'Administrace', + 'archiving' => 'Archivace', + 'authentication' => 'Přihlášení', + 'check_install' => 'Ověření instalace', + 'configuration' => 'Nastavení', + 'display' => 'Zobrazení', + 'extensions' => 'Rozšíření', + 'logs' => 'Logy', + 'queries' => 'Uživatelské dotazy', + 'reading' => 'Čtení', + 'search' => 'Hledat výraz nebo #tagy', + 'sharing' => 'Sdílení', + 'shortcuts' => 'Zkratky', + 'stats' => 'Statistika', + 'update' => 'Aktualizace', + 'user_management' => 'Správa uživatelů', + 'user_profile' => 'Profil', + ), + 'pagination' => array( + 'first' => 'První', + 'last' => 'Poslední', + 'load_more' => 'Načíst více článků', + 'mark_all_read' => 'Označit vše jako přečtené', + 'next' => 'Další', + 'nothing_to_load' => 'Žádné nové články', + 'previous' => 'Předchozí', + ), + 'share' => array( + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'Email', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'print' => 'Tisk', + 'shaarli' => 'Shaarli', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'short' => array( + 'attention' => 'Upozornění!', + 'blank_to_disable' => 'Zakázat - ponechte prázdné', + 'by_author' => 'Od %s', + 'by_default' => 'Výchozí', + 'damn' => 'Sakra!', + 'default_category' => 'Nezařazeno', + 'no' => 'Ne', + 'ok' => 'Ok!', + 'or' => 'nebo', + 'yes' => 'Ano', + ), +); diff --git a/app/i18n/cz/index.php b/app/i18n/cz/index.php new file mode 100644 index 000000000..5691d12af --- /dev/null +++ b/app/i18n/cz/index.php @@ -0,0 +1,61 @@ + array( + '_' => 'O FreshRSS', + 'agpl3' => 'AGPL 3', + 'bugs_reports' => 'Hlášení chyb', + 'credits' => 'Poděkování', + 'credits_content' => 'Některé designové prvky pocházejí z Bootstrap, FreshRSS ale tuto platformu nevyužívá. Ikony pocházejí z GNOME projektu. Font Open Sans vytvořil Steve Matteson. Favicony jsou shromažďovány pomocí getFavicon API. FreshRSS je založen na PHP framework Minz.', + 'freshrss_description' => 'FreshRSS je čtečka RSS kanálů určená k provozu na vlastním serveru, podobná Kriss Feed nebo Leed. Je to nenáročný a jednoduchý, zároveň ale mocný a konfigurovatelný nástroj.', + 'github' => 'na Github', + 'license' => 'Licence', + 'project_website' => 'Stránka projektu', + 'title' => 'O FreshRSS', + 'version' => 'Verze', + 'website' => 'Webové stránka', + ), + 'feed' => array( + 'add' => 'Můžete přidat kanály.', + 'empty' => 'Žádné články k zobrazení.', + 'rss_of' => 'RSS kanál %s', + 'title' => 'RSS kanály', + 'title_global' => 'Přehled', + 'title_fav' => 'Oblíbené', + ), + 'log' => array( + '_' => 'Logy', + 'clear' => 'Vymazat logy', + 'empty' => 'Log je prázdný', + 'title' => 'Logy', + ), + 'menu' => array( + 'about' => 'O FreshRSS', + 'add_query' => 'Vytvořit dotaz', + 'before_one_day' => 'Den nazpět', + 'before_one_week' => 'Před týdnem', + 'favorites' => 'Oblíbené (%s)', + 'global_view' => 'Přehled', + 'main_stream' => 'Všechny kanály', + 'mark_all_read' => 'Označit vše jako přečtené', + 'mark_cat_read' => 'Označit kategorii jako přečtenou', + 'mark_feed_read' => 'Označit kanál jako přečtený', + 'newer_first' => 'Nové nejdříve', + 'non-starred' => 'Zobrazit vše vyjma oblíbených', + 'normal_view' => 'Normální', + 'older_first' => 'Nejstarší nejdříve', + 'queries' => 'Uživatelské dotazy', + 'read' => 'Zobrazovat přečtené', + 'reader_view' => 'Čtení', + 'rss_view' => 'RSS kanál', + 'search_short' => 'Hledat', + 'starred' => 'Zobrazit oblíbené', + 'stats' => 'Statistika', + 'subscription' => 'Správa subskripcí', + 'unread' => 'Zobrazovat nepřečtené', + ), + 'share' => 'Sdílet', + 'tag' => array( + 'related' => 'Související tagy', + ), +); diff --git a/app/i18n/cz/install.php b/app/i18n/cz/install.php new file mode 100644 index 000000000..53257c84f --- /dev/null +++ b/app/i18n/cz/install.php @@ -0,0 +1,107 @@ + array( + 'finish' => 'Dokončit instalaci', + 'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.', + 'next_step' => 'Přejít na další krok', + ), + 'auth' => array( + 'email_persona' => 'Email pro přihlášení
    (pro Mozilla Persona)', + 'form' => 'Webový formulář (tradiční, vyžaduje JavaScript)', + 'http' => 'HTTP (pro pokročilé uživatele s HTTPS)', + 'none' => 'Žádný (nebezpečné)', + 'password_form' => 'Heslo
    (pro přihlášení webovým formulářem)', + 'password_format' => 'Alespoň 7 znaků', + 'persona' => 'Mozilla Persona (moderní, vyžaduje JavaScript)', + 'type' => 'Způsob přihlášení', + ), + 'bdd' => array( + '_' => 'Databáze', + 'conf' => array( + '_' => 'Nastavení databáze', + 'ko' => 'Ověřte informace o databázi.', + 'ok' => 'Nastavení databáze bylo uloženo.', + ), + 'host' => 'Hostitel', + 'prefix' => 'Prefix tabulky', + 'password' => 'Heslo', + 'type' => 'Typ databáze', + 'username' => 'Uživatel', + ), + 'check' => array( + '_' => 'Kontrola', + 'cache' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/cache. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře cache jsou v pořádku.', + ), + 'ctype' => array( + 'nok' => 'Není nainstalována požadovaná knihovna pro ověřování znaků (php-ctype).', + 'ok' => 'Je nainstalována požadovaná knihovna pro ověřování znaků (ctype).', + ), + 'curl' => array( + 'nok' => 'Nemáte cURL (balíček php5-curl).', + 'ok' => 'Máte rozšíření cURL.', + ), + 'data' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře data jsou v pořádku.', + ), + 'dom' => array( + 'nok' => 'Nemáte požadovanou knihovnu pro procházení DOM (balíček php-xml).', + 'ok' => 'Máte požadovanou knihovnu pro procházení DOM.', + ), + 'favicons' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/favicons. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře favicons jsou v pořádku.', + ), + 'http_referer' => array( + 'nok' => 'Zkontrolujte prosím že neměníte HTTP REFERER.', + 'ok' => 'Váš HTTP REFERER je znám a odpovídá Vašemu serveru.', + ), + 'minz' => array( + 'nok' => 'Nemáte framework Minz.', + 'ok' => 'Máte framework Minz.', + ), + 'pcre' => array( + 'nok' => 'Nemáte požadovanou knihovnu pro regulární výrazy (php-pcre).', + 'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).', + ), + 'pdo' => array( + 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', + 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/persona. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře Mozilla Persona jsou v pořádku.', + ), + 'php' => array( + 'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.', + 'ok' => 'Vaše verze PHP je %s a je kompatibilní s FreshRSS.', + ), + 'users' => array( + 'nok' => 'Zkontrolujte oprávnění adresáře ./data/users. HTTP server musí mít do tohoto adresáře práva zápisu', + 'ok' => 'Oprávnění adresáře users jsou v pořádku.', + ), + ), + 'conf' => array( + '_' => 'Obecná nastavení', + 'ok' => 'Nastavení bylo uloženo.', + ), + 'congratulations' => 'Gratulujeme!', + 'default_user' => 'Jméno výchozího uživatele (maximálně 16 alfanumerických znaků)', + 'delete_articles_after' => 'Smazat články starší než', + 'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.', + 'javascript_is_better' => 'Práce s FreshRSS je příjemnější se zapnutým JavaScriptem', + 'language' => array( + '_' => 'Jazyk', + 'choose' => 'Vyberte jazyk FreshRSS', + 'defined' => 'Jazyk byl nastaven.', + ), + 'not_deleted' => 'Nastala chyba, soubor %s musíte smazat ručně.', + 'ok' => 'Instalace byla úspěšná.', + 'step' => 'krok %d', + 'steps' => 'Kroky', + 'title' => 'Instalace · FreshRSS', + 'this_is_the_end' => 'Konec', +); diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php new file mode 100644 index 000000000..d7ff63fe9 --- /dev/null +++ b/app/i18n/cz/sub.php @@ -0,0 +1,61 @@ + array( + '_' => 'Kategorie', + 'add' => 'Přidat kategorii', + 'empty' => 'Vyprázdit kategorii', + 'new' => 'Nová kategorie', + ), + 'feed' => array( + 'add' => 'Přidat RSS kanál', + 'advanced' => 'Pokročilé', + 'archiving' => 'Archivace', + 'auth' => array( + 'configuration' => 'Přihlášení', + 'help' => 'Umožní přístup k RSS kanálům chráneným HTTP autentizací', + 'http' => 'HTTP přihlášení', + 'password' => 'Heslo', + 'username' => 'Přihlašovací jméno', + ), + 'css_help' => 'Stáhne zkrácenou verzi RSS kanálů (pozor, náročnější na čas!)', + 'css_path' => 'Původní CSS soubor článku z webových stránek', + 'description' => 'Popis', + 'empty' => 'Kanál je prazdný. Ověřte prosím zda je ještě autorem udržován.', + 'error' => 'Vyskytl se problém s kanálem. Ověřte že je vždy dostupný, prosím, a poté jej aktualizujte.', + 'in_main_stream' => 'Zobrazit ve “Všechny kanály”', + 'informations' => 'Informace', + 'keep_history' => 'Zachovat tento minimální počet článků', + 'moved_category_deleted' => 'Po smazání kategorie budou v ní obsažené kanály automaticky přesunuty do %s.', + 'no_selected' => 'Nejsou označeny žádné kanály.', + 'number_entries' => '%d článků', + 'stats' => 'Statistika', + 'think_to_add' => 'Můžete přidat kanály.', + 'title' => 'Název', + 'title_add' => 'Přidat RSS kanál', + 'ttl' => 'Neobnovovat častěji než', + 'url' => 'URL kanálu', + 'validator' => 'Zkontrolovat platnost kanálu', + 'website' => 'URL webové stránky', + ), + 'import_export' => array( + 'export' => 'Export', + 'export_opml' => 'Exportovat seznam kanálů (OPML)', + 'export_starred' => 'Exportovat oblíbené', + 'feed_list' => 'Seznam %s článků', + 'file_to_import' => 'Soubor k importu
    (OPML, Json nebo Zip)', + 'file_to_import_no_zip' => 'Soubor k importu
    (OPML nebo Json)', + 'import' => 'Import', + 'starred_list' => 'Seznam oblíbených článků', + 'title' => 'Import / export', + ), + 'menu' => array( + 'bookmark' => 'Přihlásit (FreshRSS bookmark)', + 'import_export' => 'Import / export', + 'subscription_management' => 'Správa subskripcí', + ), + 'title' => array( + '_' => 'Správa subskripcí', + 'feed_management' => 'Správa RSS kanálů', + ), +); From 91e5231b26acd330ff6c47e3de7b9979158d45a6 Mon Sep 17 00:00:00 2001 From: Tets Date: Fri, 27 Mar 2015 08:13:34 +0100 Subject: [PATCH 40/90] Added Czech translation --- app/i18n/de/gen.php | 1 + app/i18n/en/gen.php | 1 + app/i18n/fr/gen.php | 1 + 3 files changed, 3 insertions(+) diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index 1b30a5be4..8970d5003 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -104,6 +104,7 @@ return array( 'should_be_activated' => 'JavaScript muss aktiviert sein', ), 'lang' => array( + 'cz' => 'Čeština', 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index fdca01a43..b02b9f0f2 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -104,6 +104,7 @@ return array( 'should_be_activated' => 'JavaScript must be enabled', ), 'lang' => array( + 'cz' => 'Čeština', 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 48fc4a7e9..c81e57bf7 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -104,6 +104,7 @@ return array( 'should_be_activated' => 'Le JavaScript doit être activé.', ), 'lang' => array( + 'cz' => 'Čeština', 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', From 6283fe99b2ded06d05d01c595a1d067010b4b886 Mon Sep 17 00:00:00 2001 From: Tets Date: Fri, 27 Mar 2015 08:13:34 +0100 Subject: [PATCH 41/90] Added Czech to the list in other languages in gen.php --- app/i18n/de/gen.php | 1 + app/i18n/en/gen.php | 1 + app/i18n/fr/gen.php | 1 + 3 files changed, 3 insertions(+) diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index 1b30a5be4..8970d5003 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -104,6 +104,7 @@ return array( 'should_be_activated' => 'JavaScript muss aktiviert sein', ), 'lang' => array( + 'cz' => 'Čeština', 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index fdca01a43..b02b9f0f2 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -104,6 +104,7 @@ return array( 'should_be_activated' => 'JavaScript must be enabled', ), 'lang' => array( + 'cz' => 'Čeština', 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 48fc4a7e9..c81e57bf7 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -104,6 +104,7 @@ return array( 'should_be_activated' => 'Le JavaScript doit être activé.', ), 'lang' => array( + 'cz' => 'Čeština', 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', From 711530a512b370d79b079205ce1f8376174f7f03 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 4 Apr 2015 22:39:31 +0200 Subject: [PATCH 42/90] 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(-) 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) { From 2d18910d02d92098257b96766e5b89a780daab0b Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 5 Apr 2015 12:41:16 +0200 Subject: [PATCH 43/90] Support for Internationalized Domain Names (IDN) https://github.com/FreshRSS/FreshRSS/issues/819 Add explicit conversion from IDN to Punycode. Requires PHP 5.3 IDN extension http://php.net/intl.idn (php5-idn package on Debian/Ubuntu). For systems without PHP 5.3+ IDN extension, we may consider adding a dependency (322 kB) to the third-party library https://phlymail.com/en/downloads/idna-convert.html See PHP bug 53474 FILTER_VALIDATE_URL should not fail URL's that use IDNhttps://bugs.php.net/bug.php?id=53474 --- README.fr.md | 2 +- README.md | 2 +- lib/lib_rss.php | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.fr.md b/README.fr.md index 380d7bc1e..ea5b07a9b 100644 --- a/README.fr.md +++ b/README.fr.md @@ -32,7 +32,7 @@ Privilégiez pour cela des demandes sur GitHub * Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées) * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres) * PHP 5.2.1+ (PHP 5.3.7+ recommandé) - * Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (seulement pour accès API sur platformes < 64 bits) + * Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (pour accès API sur platformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés) * Recommandés : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip) * MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ * Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ diff --git a/README.md b/README.md index a09a64639..92072b07d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The best way is to open issues on GitHub * It even works on Raspberry Pi with response time under a second (tested with 150 feeds, 22k articles, or 32Mo of compressed data) * A web server: Apache2 (recommanded), nginx, lighttpd (not tested on others) * PHP 5.2.1+ (PHP 5.3.7+ recommanded) - * Required extensions: [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (only for API access on platforms under 64 bits) + * Required extensions: [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names) * Recommanded extensions : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip) * MySQL 5.0.3+ (recommanded) or SQLite 3.7.4+ * A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ diff --git a/lib/lib_rss.php b/lib/lib_rss.php index e5fe73041..bc5d6fc5b 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -51,6 +51,21 @@ function classAutoloader($class) { spl_autoload_register('classAutoloader'); // +function idn_to_punny($url) { + if (function_exists('idn_to_ascii')) { + $parts = parse_url($url); + if (!empty($parts['host'])) { + $idn = $parts['host']; + $punny = idn_to_ascii($idn); + $pos = strpos($url, $idn); + if ($pos !== false) { + return substr_replace($url, $punny, $pos, strlen($idn)); + } + } + } + return $url; +} + function checkUrl($url) { if (empty ($url)) { return ''; @@ -58,6 +73,7 @@ function checkUrl($url) { if (!preg_match ('#^https?://#i', $url)) { $url = 'http://' . $url; } + $url = idn_to_punny($url); //PHP bug #53474 IDN if (filter_var($url, FILTER_VALIDATE_URL) || (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($url, '-') > 0) && //PHP bug #51192 ($url === filter_var($url, FILTER_SANITIZE_URL)))) { From 06b76831dece01f836c9d0a3cc32c3f59910fe60 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 5 Apr 2015 13:07:34 +0200 Subject: [PATCH 44/90] Punycode spelling mistake https://github.com/FreshRSS/FreshRSS/pull/820 --- data/subscriptions/.gitignore | 1 + lib/lib_rss.php | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 data/subscriptions/.gitignore diff --git a/data/subscriptions/.gitignore b/data/subscriptions/.gitignore new file mode 100644 index 000000000..150f68c80 --- /dev/null +++ b/data/subscriptions/.gitignore @@ -0,0 +1 @@ +*/* diff --git a/lib/lib_rss.php b/lib/lib_rss.php index bc5d6fc5b..c4f6a6011 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -51,15 +51,15 @@ function classAutoloader($class) { spl_autoload_register('classAutoloader'); // -function idn_to_punny($url) { +function idn_to_puny($url) { if (function_exists('idn_to_ascii')) { $parts = parse_url($url); if (!empty($parts['host'])) { $idn = $parts['host']; - $punny = idn_to_ascii($idn); + $puny = idn_to_ascii($idn); $pos = strpos($url, $idn); if ($pos !== false) { - return substr_replace($url, $punny, $pos, strlen($idn)); + return substr_replace($url, $puny, $pos, strlen($idn)); } } } @@ -73,7 +73,7 @@ function checkUrl($url) { if (!preg_match ('#^https?://#i', $url)) { $url = 'http://' . $url; } - $url = idn_to_punny($url); //PHP bug #53474 IDN + $url = idn_to_puny($url); //PHP bug #53474 IDN if (filter_var($url, FILTER_VALIDATE_URL) || (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($url, '-') > 0) && //PHP bug #51192 ($url === filter_var($url, FILTER_SANITIZE_URL)))) { From 8ea9cdd703179cddaebfb35ab4584565b93d3507 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 5 Apr 2015 13:11:50 +0200 Subject: [PATCH 45/90] Revert file supposed to be in another branch --- data/subscriptions/.gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 data/subscriptions/.gitignore diff --git a/data/subscriptions/.gitignore b/data/subscriptions/.gitignore deleted file mode 100644 index 150f68c80..000000000 --- a/data/subscriptions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*/* From d229216cccbd746b46630a44fa508ef0367ea1a1 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 22 Apr 2015 00:24:22 -0400 Subject: [PATCH 46/90] 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(-) 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'), ), ); } From 7f7de31c1dcb6599be5c5713f36b4bde1d03d47a Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 9 May 2015 13:07:54 +0200 Subject: [PATCH 47/90] 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(-) 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(); From 993466844405bd9854d890d6d5ebf763ed8b78cb Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 9 May 2015 23:37:56 +0200 Subject: [PATCH 48/90] 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(-) 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'; From a7bc54bb996a87a66127101be548d181dc8dd935 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 9 May 2015 23:45:52 +0200 Subject: [PATCH 49/90] Minor spaces --- app/Models/FeedDAO.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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'; From fedf062b4942a1e30608b662f713f06a83a0565c Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 11:50:08 +0200 Subject: [PATCH 50/90] Comments in config file Documentation https://github.com/FreshRSS/FreshRSS/issues/123 --- README.fr.md | 1 + README.md | 1 + data/config.default.php | 72 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/README.fr.md b/README.fr.md index ea5b07a9b..6c77ccf51 100644 --- a/README.fr.md +++ b/README.fr.md @@ -46,6 +46,7 @@ Privilégiez pour cela des demandes sur GitHub 3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/` 4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation 5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à me contacter. +6. Des paramètres de configuration avancée peuvent être accédés depuis [config.php](./data/config.default.php). # Contrôle d’accès Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix : diff --git a/README.md b/README.md index 92072b07d..089bbd780 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ The best way is to open issues on GitHub 3. Add write access on `./data/` folder to the webserver user 4. Access FreshRSS with your browser and follow the installation process 5. Every thing should be working :) If you encounter any problem, feel free to contact me. +6. Advanced configuration settings can be seen in [config.php](./data/config.default.php). # Access control It is needed for the multi-user mode to limit access to FreshRSS. You can: diff --git a/data/config.default.php b/data/config.default.php index 839bd1687..8be203d36 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -1,32 +1,104 @@ 'production', + + # Used to make crypto more unique. Generated during install. 'salt' => '', + + # Leave empty for most cases. + # Ability to override the address of the FreshRSS instance, + # used when building absolute URLs. 'base_url' => '', + + # Natural language of the user interface, e.g. `en`, `fr`. 'language' => 'en', + + # Title of this FreshRSS instance in the Web user interface. 'title' => 'FreshRSS', + + # Name of the user that has administration rights. 'default_user' => '_', + + # Allow or not visitors without login to see the articles + # of the default user. 'allow_anonymous' => false, + + # Allow or not anonymous users to start the refresh process. 'allow_anonymous_refresh' => false, + + # Login method: + # `none` is without password and shows only the default user; + # `form` is a conventional Web login form; + # `persona` is the email-based login by Mozilla; + # `http_auth` is an access controled by the HTTP Web server (e.g. `/FreshRSS/p/i/.htaccess` for Apache) + # if you use `http_auth`, remember to protect only `/FreshRSS/p/i/`, + # and in particular not protect `/FreshRSS/p/api/` if you would like to use the API (different login system). 'auth_type' => 'none', + + # Allow or not the use of the API, used for mobile apps. + # End-point is http://example.net/FreshRSS/p/api/greader.php + # You need to set the user's API password. 'api_enabled' => false, + + # Allow or not the use of an unsafe login, + # by providing username and password in the login URL: + # http://example.net/FreshRSS/p/i/?c=auth&a=login&u=alice&p=1234 'unsafe_autologin_enabled' => false, + + # Enable or not the use of syslog to log the activity of + # SimplePie, which is retrieving RSS feeds via HTTP requests. 'simplepie_syslog_enabled' => true, + 'limits' => array( + + # Duration in seconds of the SimplePie cache, + # during which a query to the RSS feed will return the local cached version. + # Especially important for multi-user setups. 'cache_duration' => 800, + + # SimplePie HTTP request timeout in seconds. 'timeout' => 10, + + # If a user has not used FreshRSS for more than x seconds, + # then its feeds are not refreshed anymore. 'max_inactivity' => PHP_INT_MAX, + + # Max number of feeds for a user. 'max_feeds' => 16384, + + # Max number of categories for a user. 'max_categories' => 16384, + ), + 'db' => array( + + # Type of database: `sqlite` or `mysql`. 'type' => 'sqlite', + + # MySQL host. 'host' => '', + + # MySQL user. 'user' => '', + + # MySQL password. 'password' => '', + + # MySQL database. 'base' => '', + + # MySQL table prefix. 'prefix' => '', + ), + + # List of enabled FreshRSS extensions. 'extensions_enabled' => array(), ); From 5f545dfda2b6700579b856c815e1914a2dd62553 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 12:14:38 +0200 Subject: [PATCH 51/90] Global option to mark updated articles as unread https://github.com/FreshRSS/FreshRSS/issues/798 --- app/Controllers/feedController.php | 3 ++- data/config.default.php | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 5657d4a88..03f438888 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -342,7 +342,8 @@ 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(null); //Change is_read according to policy. //TODO: Implement option + //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. if (!$entryDAO->hasTransaction()) { $entryDAO->beginTransaction(); } diff --git a/data/config.default.php b/data/config.default.php index 8be203d36..dc947f154 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -55,6 +55,10 @@ 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, From 0d0c6b7493161d350ca2eb1d4c45c0c70cfcbb92 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 14:04:12 +0200 Subject: [PATCH 52/90] 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(-) 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 ( From 79f0f2bbb48f5f4fb1ebd9350bf9bf6e1182e6cf Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 18:21:21 +0200 Subject: [PATCH 53/90] =?UTF-8?q?Bug=20Page=20403=20ne=20peut=20s'afficher?= =?UTF-8?q?=20si=20Translate=20n'est=20pas=20instanci=C3=A9=20avant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/FreshRSS/FreshRSS/issues/821 --- app/Controllers/feedController.php | 2 +- app/FreshRSS.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index a36a38ce2..0443b4159 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -146,7 +146,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $name = $feed->name(); $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); if ($feed === null) { - Minz_Request::bad(_t('feed_not_added', $name), $url_redirect); + Minz_Request::bad(_t('feedback.sub.feed.not_added', $name), $url_redirect); } $values = array( diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 021687999..044de9cd4 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -63,10 +63,11 @@ class FreshRSS extends Minz_FrontController { // Basic protection against XSRF attacks FreshRSS_Auth::removeAccess(); $http_referer = empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']; + Minz_Translate::init('en'); //TODO: Better choice of fallback language Minz_Error::error( 403, array('error' => array( - _t('access_denied'), + _t('feedback.access.denied'), ' [HTTP_REFERER=' . htmlspecialchars($http_referer) . ']' )) ); From f7a502b06e7b0fe0b8bbc5f41d96d4ea08eee98a Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 19:20:46 +0200 Subject: [PATCH 54/90] Cannot create an account with sqlite https://github.com/FreshRSS/FreshRSS/issues/770 --- app/install.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/install.php b/app/install.php index 177173fdb..5f43b6415 100644 --- a/app/install.php +++ b/app/install.php @@ -741,6 +741,13 @@ function printStep3() { From 0745252b68f6f9b7c91ea437893b5f33b7a224c3 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 20:31:03 +0200 Subject: [PATCH 55/90] 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(-) 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); } From ead849dbe3717966dff50e55d6bf849dafde87b7 Mon Sep 17 00:00:00 2001 From: Tets Date: Mon, 11 May 2015 10:20:17 +0200 Subject: [PATCH 56/90] Czech translation --- app/i18n/cz/feedback.php | 2 +- app/i18n/cz/sub.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/i18n/cz/feedback.php b/app/i18n/cz/feedback.php index 52ff029ef..b75a4a15a 100644 --- a/app/i18n/cz/feedback.php +++ b/app/i18n/cz/feedback.php @@ -11,7 +11,7 @@ return array( 'auth' => array( 'form' => array( 'not_set' => 'Nastal problém s konfigurací přihlašovacího systému. Zkuste to prosím později.', - 'set' => 'Webové formulář je nyní výchozí přihlašovací systém.', + 'set' => 'Webový formulář je nyní výchozí přihlašovací systém.', ), 'login' => array( 'invalid' => 'Login není platný', diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index d7ff63fe9..78712506c 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -21,7 +21,7 @@ return array( 'css_help' => 'Stáhne zkrácenou verzi RSS kanálů (pozor, náročnější na čas!)', 'css_path' => 'Původní CSS soubor článku z webových stránek', 'description' => 'Popis', - 'empty' => 'Kanál je prazdný. Ověřte prosím zda je ještě autorem udržován.', + 'empty' => 'Kanál je prázdný. Ověřte prosím zda je ještě autorem udržován.', 'error' => 'Vyskytl se problém s kanálem. Ověřte že je vždy dostupný, prosím, a poté jej aktualizujte.', 'in_main_stream' => 'Zobrazit ve “Všechny kanály”', 'informations' => 'Informace', From 217c191a1ba3ac03b847d261a32e19975380fcad Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Mon, 11 May 2015 22:42:41 +0200 Subject: [PATCH 57/90] 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(-) 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); + } } From 5adaf177210bcd7af0df30d8bebea9b3de67b443 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 15 May 2015 15:38:54 +0200 Subject: [PATCH 58/90] Revert bug HTTP 301 introduced by Refactor feedController https://github.com/FreshRSS/FreshRSS/commit/e2da6e6e6b871dc3dbc289cdd40ba401a21d8e91 --- app/Controllers/feedController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 0443b4159..8db273aca 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -301,6 +301,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { continue; } + $url = $feed->url(); //For detection of HTTP 301 try { // Load entries $feed->load(false); @@ -311,7 +312,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { continue; } - $url = $feed->url(); $feed_history = $feed->keepHistory(); if ($feed_history == -2) { // TODO: -2 must be a constant! From 9fe8a7c62dcd815065983613991f000b17444f5c Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Fri, 15 May 2015 20:09:58 +0200 Subject: [PATCH 59/90] Update admin.php --- app/i18n/de/admin.php | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php index bcd0fcc61..8550805ee 100644 --- a/app/i18n/de/admin.php +++ b/app/i18n/de/admin.php @@ -19,15 +19,15 @@ return array( 'check_install' => array( 'cache' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/cache. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/cache sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/cache sind in Ordnung.', ), 'categories' => array( 'nok' => 'Die Tabelle category ist schlecht konfiguriert.', - 'ok' => 'Die Tabelle category ist in Ordnung.', + 'ok' => 'Die Tabelle category ist korrekt konfiguriert.', ), 'connection' => array( 'nok' => 'Verbindung zur Datenbank kann nicht aufgebaut werden.', - 'ok' => 'Verbindung zur Datenbank ist in Ordnung.', + 'ok' => 'Verbindung zur Datenbank konnte aufgebaut werden.', ), 'ctype' => array( 'nok' => 'Ihnen fehlt eine benötigte Bibliothek für die Überprüfung von Zeichentypen (php-ctype).', @@ -39,7 +39,7 @@ return array( ), 'data' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data sind in Ordnung.', ), 'database' => 'Datenbank-Installation', 'dom' => array( @@ -48,19 +48,19 @@ return array( ), 'entries' => array( 'nok' => 'Die Tabelle entry ist schlecht konfiguriert.', - 'ok' => 'Die Tabelle entry ist in Ordnung.', + 'ok' => 'Die Tabelle entry ist korrekt konfiguriert.', ), 'favicons' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/favicons. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/favicons sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/favicons sind in Ordnung.', ), 'feeds' => array( 'nok' => 'Die Tabelle feed ist schlecht konfiguriert.', - 'ok' => 'Die Tabelle feed ist in Ordnung.', + 'ok' => 'Die Tabelle feed ist korrekt konfiguriert.', ), 'files' => 'Datei-Installation', 'json' => array( - 'nok' => 'Ihnen fehlt JSON (Paket php5-json).', + 'nok' => 'Ihnen fehlt die JSON-Erweiterung (Paket php5-json).', 'ok' => 'Sie haben die JSON-Erweiterung.', ), 'minz' => array( @@ -77,7 +77,7 @@ return array( ), 'persona' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/persona. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/persona sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/persona sind in Ordnung.', ), 'php' => array( '_' => 'PHP-Installation', @@ -91,11 +91,11 @@ return array( 'title' => 'Installationsüberprüfung', 'tokens' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/tokens. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/tokens sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/tokens sind in Ordnung.', ), 'users' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/users. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/users sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/users sind in Ordnung.', ), 'zip' => array( 'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php5-zip).', @@ -120,22 +120,22 @@ return array( 'category' => 'Kategorie', 'entry_count' => 'Anzahl der Einträge', 'entry_per_category' => 'Einträge pro Kategorie', - 'entry_per_day' => 'Einträge pro Tag (letzte 30 Tage)', + 'entry_per_day' => 'Einträge pro Tag (letzten 30 Tage)', 'entry_per_day_of_week' => 'Pro Wochentag (Durchschnitt: %.2f Nachrichten)', 'entry_per_hour' => 'Pro Stunde (Durchschnitt: %.2f Nachrichten)', 'entry_per_month' => 'Pro Monat (Durchschnitt: %.2f Nachrichten)', 'entry_repartition' => 'Einträge-Verteilung', 'feed' => 'Feed', 'feed_per_category' => 'Feeds pro Kategorie', - 'idle' => 'Untätige Feeds', + 'idle' => 'Inkative Feeds', 'main' => 'Haupt-Statistiken', 'main_stream' => 'Haupt-Feeds', 'menu' => array( - 'idle' => 'Untätige Feeds', + 'idle' => 'Inkative Feeds', 'main' => 'Haupt-Statistiken', 'repartition' => 'Artikel-Verteilung', ), - 'no_idle' => 'Es gibt keinen untätigen Feed!', + 'no_idle' => 'Es gibt keinen inaktiven Feed!', 'number_entries' => '%d Artikel', 'percent_of_total' => '%% Gesamt', 'repartition' => 'Artikel-Verteilung', @@ -152,7 +152,7 @@ return array( 'check' => 'Auf neue Aktualisierungen prüfen', 'current_version' => 'Ihre aktuelle Version von FreshRSS ist %s.', 'last' => 'Letzte Überprüfung: %s', - 'none' => 'Keine Aktualisierung zum Anwenden', + 'none' => 'Keine ausstehende Aktualisierung', 'title' => 'System aktualisieren', ), 'user' => array( From bd21838bd0edd0169c583d25a475f9eea7636333 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Fri, 15 May 2015 20:18:24 +0200 Subject: [PATCH 60/90] Update conf.php --- app/i18n/de/conf.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php index df2c07d49..4a0a77ddd 100644 --- a/app/i18n/de/conf.php +++ b/app/i18n/de/conf.php @@ -5,8 +5,8 @@ return array( '_' => 'Archivierung', 'advanced' => 'Erweitert', 'delete_after' => 'Entferne Artikel nach', - 'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Nachrichten-Feeds vorhanden.', - 'keep_history_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten wird', + 'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Feeds verfügbar.', + 'keep_history_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden', 'optimize' => 'Datenbank optimieren', 'optimize_help' => 'Sollte gelegentlich durchgeführt werden, um die Größe der Datenbank zu reduzieren.', 'purge_now' => 'Jetzt bereinigen', @@ -32,10 +32,10 @@ return array( 'title' => 'Anzeige', 'width' => array( 'content' => 'Inhaltsbreite', - 'large' => 'Weit', + 'large' => 'Gross', 'medium' => 'Mittel', 'no_limit' => 'Keine Begrenzung', - 'thin' => 'Schmal', + 'thin' => 'Klein', ), ), 'query' => array( @@ -136,14 +136,14 @@ return array( 'wallabag' => 'wallabag', ), 'shortcut' => array( - '_' => 'Tastaturkürzel', + '_' => 'Tastenkombination', 'article_action' => 'Artikelaktionen', 'auto_share' => 'Teilen', 'auto_share_help' => 'Wenn es nur eine Option zum Teilen gibt, wird diese verwendet. Ansonsten sind die Optionen über ihre Nummer erreichbar.', 'close_dropdown' => 'Menüs schließen', - 'collapse_article' => 'Zusammenfalten', + 'collapse_article' => 'Einklappen', 'first_article' => 'Zum ersten Artikel springen', - 'focus_search' => 'Auf Suchfeld zugreifen', + 'focus_search' => 'Auf das Suchfeld zugreifen', 'help' => 'Dokumentation anzeigen', 'javascript' => 'JavaScript muss aktiviert sein, um Tastaturkürzel benutzen zu können', 'last_article' => 'Zum letzten Artikel springen', @@ -151,13 +151,13 @@ return array( 'mark_read' => 'Als gelesen markieren', 'mark_favorite' => 'Als Favorit markieren', 'navigation' => 'Navigation', - 'navigation_help' => 'Mit der "Umschalttaste" finden die Tastaturkürzel auf Feeds Anwendung.
    Mit der "Alt-Taste" finden die Tastaturkürzel auf Kategorien Anwendung.', + 'navigation_help' => 'Mit der "Umschalttaste" finden die Tastenkombination auf Feeds Anwendung.
    Mit der "Alt-Taste" finden die Tastenkombination auf Kategorien Anwendung.', 'next_article' => 'Zum nächsten Artikel springen', 'other_action' => 'Andere Aktionen', 'previous_article' => 'Zum vorherigen Artikel springen', 'see_on_website' => 'Auf der Original-Webseite ansehen', 'shift_for_all_read' => '+ Umschalttaste, um alle Artikel als gelesen zu markieren.', - 'title' => 'Tastaturkürzel', + 'title' => 'Tastenkombination', 'user_filter' => 'Auf Benutzerfilter zugreifen', 'user_filter_help' => 'Wenn es nur einen Benutzerfilter gibt, wird dieser verwendet. Ansonsten sind die Filter über ihre Nummer erreichbar.', ), From 70f0fc8d614ae49aa21ef9f1ce1b3f06e294512f Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:52:08 +0200 Subject: [PATCH 61/90] Update feedback.php --- app/i18n/de/feedback.php | 60 ++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/app/i18n/de/feedback.php b/app/i18n/de/feedback.php index 48f8b74f5..4c15aadc3 100644 --- a/app/i18n/de/feedback.php +++ b/app/i18n/de/feedback.php @@ -15,19 +15,19 @@ return array( ), 'login' => array( 'invalid' => 'Anmeldung ist ungültig', - 'success' => 'Sie sind verbunden', + 'success' => 'Sie sind angemeldet', ), 'logout' => array( - 'success' => 'Sie sind getrennt', + 'success' => 'Sie sind abgemeldet', ), 'no_password_set' => 'Administrator-Passwort ist nicht gesetzt worden. Dieses Feature ist nicht verfügbar.', 'not_persona' => 'Nur das Persona-System kann zurückgesetzt werden.', ), 'conf' => array( - 'error' => 'Während des Speicherung der Konfiguration trat ein Fehler auf', + 'error' => 'Während der Speicherung der Konfiguration trat ein Fehler auf', 'query_created' => 'Abfrage "%s" ist erstellt worden.', - 'shortcuts_updated' => 'Tastaturkürzel sind aktualisiert worden', - 'updated' => 'Konfiguration ist aktualisiert worden', + 'shortcuts_updated' => 'Die Tastenkombinationen sind aktualisiert worden', + 'updated' => 'Die Konfiguration ist aktualisiert worden', ), 'extensions' => array( 'already_enabled' => '%s ist bereits aktiviert', @@ -44,44 +44,44 @@ return array( 'not_found' => '%s existiert nicht', ), 'import_export' => array( - 'export_no_zip_extension' => 'Die Zip-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie, Dateien eine nach der anderen zu exportieren.', + 'export_no_zip_extension' => 'Die Zip-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie die Dateien eine nach der anderen zu exportieren.', 'feeds_imported' => 'Ihre Feeds sind importiert worden und werden jetzt aktualisiert', 'feeds_imported_with_errors' => 'Ihre Feeds sind importiert worden, aber es traten einige Fehler auf', - 'file_cannot_be_uploaded' => 'Datei kann nicht hochgeladen werden!', + 'file_cannot_be_uploaded' => 'Die Datei kann nicht hochgeladen werden!', 'no_zip_extension' => 'Die Zip-Erweiterung ist auf Ihrem Server nicht vorhanden.', 'zip_error' => 'Ein Fehler trat während des Zip-Imports auf.', ), 'sub' => array( 'actualize' => 'Aktualisieren', 'category' => array( - 'created' => 'Kategorie %s ist erstellt worden.', - 'deleted' => 'Kategorie ist gelöscht worden.', - 'emptied' => 'Kategorie ist geleert worden.', - 'error' => 'Kategorie kann nicht aktualisiert werden', - 'name_exists' => 'Kategorie-Name existiert bereits.', + 'created' => 'Die Kategorie %s ist erstellt worden.', + 'deleted' => 'Die Kategorie ist gelöscht worden.', + 'emptied' => 'Die Kategorie ist geleert worden.', + 'error' => 'Die Kategorie kann nicht aktualisiert werden', + 'name_exists' => 'Der Kategorie-Name existiert bereits.', 'no_id' => 'Sie müssen die ID der Kategorie präzisieren.', - 'no_name' => 'Kategorie-Name kann nicht leer sein.', + 'no_name' => 'Der Kategorie-Name kann nicht leer sein.', 'not_delete_default' => 'Sie können die Vorgabe-Kategorie nicht löschen!', 'not_exist' => 'Die Kategorie existiert nicht!', - 'over_max' => 'Sie haben Ihr Kategorien-Limit erreicht (%d)', - 'updated' => 'Kategorie ist aktualisiert worden.', + 'over_max' => 'Sie haben Ihre Kategorien-Limite erreicht (%d)', + 'updated' => 'Die Kategorie ist aktualisiert worden.', ), 'feed' => array( 'actualized' => '%s ist aktualisiert worden', - 'actualizeds' => 'RSS-Feeds sind aktualisiert worden', - 'added' => 'RSS-Feed %s ist hinzugefügt worden', + 'actualizeds' => 'Die RSS-Feeds sind aktualisiert worden', + 'added' => 'Der RSS-Feed %s ist hinzugefügt worden', 'already_subscribed' => 'Sie haben %s bereits abonniert', - 'deleted' => 'Feed ist gelöscht worden', - 'error' => 'Feed kann nicht aktualisiert werden', + 'deleted' => 'Der Feed ist gelöscht worden', + 'error' => 'Der Feed kann nicht aktualisiert werden', 'internal_problem' => 'Der RSS-Feed konnte nicht hinzugefügt werden. Für Details prüfen Sie die FressRSS-Protokolle.', - 'invalid_url' => 'URL %s ist ungültig', - 'marked_read' => 'Feeds sind als gelesen markiert worden', - 'n_actualized' => '%d Feeds sind aktualisiert worden', - 'n_entries_deleted' => '%d Artikel sind gelöscht worden', + 'invalid_url' => 'Die URL %s ist ungültig', + 'marked_read' => 'Die Feeds sind als gelesen markiert worden', + 'n_actualized' => 'Die %d Feeds sind aktualisiert worden', + 'n_entries_deleted' => 'Die %d Artikel sind gelöscht worden', 'no_refresh' => 'Es gibt keinen Feed zum Aktualisieren…', 'not_added' => '%s konnte nicht hinzugefügt werden', - 'over_max' => 'Sie haben Ihr Feeds-Limit erreicht (%d)', - 'updated' => 'Feed ist aktualisiert worden', + 'over_max' => 'Sie haben Ihre Feeds-Limite erreicht (%d)', + 'updated' => 'Der Feed ist aktualisiert worden', ), 'purge_completed' => 'Bereinigung abgeschlossen (%d Artikel gelöscht)', ), @@ -91,16 +91,16 @@ return array( 'file_is_nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses %s. Der HTTP-Server muss Schreibrechte besitzen', 'finished' => 'Aktualisierung abgeschlossen!', 'none' => 'Keine Aktualisierung zum Anwenden', - 'server_not_found' => 'Aktualisierungs-Server kann nicht gefunden werden. [%s]', + 'server_not_found' => 'Der Aktualisierungs-Server kann nicht gefunden werden. [%s]', ), 'user' => array( 'created' => array( - '_' => 'Benutzer %s ist erstellt worden', - 'error' => 'Benutzer %s kann nicht erstellt werden', + '_' => 'Der Benutzer %s ist erstellt worden', + 'error' => 'Der Benutzer %s kann nicht erstellt werden', ), 'deleted' => array( - '_' => 'Benutzer %s ist gelöscht worden', - 'error' => 'Benutzer %s kann nicht gelöscht werden', + '_' => 'Der Benutzer %s ist gelöscht worden', + 'error' => 'Der Benutzer %s kann nicht gelöscht werden', ), ), 'profile' => array( From 7c6ce30f59295d06ad7fc7f6c3e935d058836877 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:54:23 +0200 Subject: [PATCH 62/90] Update gen.php --- app/i18n/de/gen.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index 8970d5003..f24a52c2e 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -49,7 +49,7 @@ return array( 'april' => 'April', 'aug' => 'Aug', 'august' => 'August', - 'before_yesterday' => 'Vor gestern', + 'before_yesterday' => 'Vor vorgestern', 'dec' => 'Dez', 'december' => 'Dezember', 'feb' => 'Feb', @@ -93,7 +93,7 @@ return array( ), 'js' => array( 'category_empty' => 'Kategorie leeren', - 'confirm_action' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Dies kann nicht abgebrochen werden!', + 'confirm_action' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Diese Aktion kann nicht abgebrochen werden!', 'confirm_action_feed_cat' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Sie werden zugehörige Favoriten und Benutzerabfragen verlieren. Dies kann nicht abgebrochen werden!', 'feedback' => array( 'body_new_articles' => 'Es gibt \\d neue Artikel zum Lesen auf FreshRSS.', From 450cc2d66f55817f85090ab742ebc351df9cbc43 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:55:31 +0200 Subject: [PATCH 63/90] Update index.php --- app/i18n/de/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/de/index.php b/app/i18n/de/index.php index 3449de87d..04798cdce 100644 --- a/app/i18n/de/index.php +++ b/app/i18n/de/index.php @@ -17,7 +17,7 @@ return array( ), 'feed' => array( 'add' => 'Sie können Feeds hinzufügen.', - 'empty' => 'Es gibt keinen Artikel zum Zeigen.', + 'empty' => 'Es gibt keinen Artikel zum Anzeigen.', 'rss_of' => 'RSS-Feed von %s', 'title' => 'Ihre RSS-Feeds', 'title_global' => 'Globale Ansicht', From fe2f6c74b9b2f5ed0f4c3f57b4bf2828e38e1f6a Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:58:57 +0200 Subject: [PATCH 64/90] Update install.php --- app/i18n/de/install.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/i18n/de/install.php b/app/i18n/de/install.php index e9267bbbd..a5899bb52 100644 --- a/app/i18n/de/install.php +++ b/app/i18n/de/install.php @@ -3,8 +3,8 @@ return array( 'action' => array( 'finish' => 'Installation fertigstellen', - 'fix_errors_before' => 'Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.', - 'next_step' => 'Zum nächsten Schritt gehen', + 'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.', + 'next_step' => 'Zum nächsten Schritt springen', ), 'auth' => array( 'email_persona' => 'Anmelde-E-Mail-Adresse
    (für Mozilla Persona)', @@ -33,7 +33,7 @@ return array( '_' => 'Überprüfungen', 'cache' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/cache. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/cache sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/cache sind in Ordnung.', ), 'ctype' => array( 'nok' => 'Ihnen fehlt eine benötigte Bibliothek für die Überprüfung von Zeichentypen (php-ctype).', @@ -45,7 +45,7 @@ return array( ), 'data' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data sind in Ordnung.', ), 'dom' => array( 'nok' => 'Ihnen fehlt eine benötigte Bibliothek um DOM zu durchstöbern (Paket php-xml).', @@ -53,7 +53,7 @@ return array( ), 'favicons' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/favicons. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/favicons sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/favicons sind in Ordnung.', ), 'http_referer' => array( 'nok' => 'Bitte stellen Sie sicher, dass Sie Ihren HTTP REFERER nicht abändern.', @@ -73,7 +73,7 @@ return array( ), 'persona' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/persona. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/persona sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/persona sind in Ordnung.', ), 'php' => array( 'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.', @@ -81,22 +81,22 @@ return array( ), 'users' => array( 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/users. Der HTTP-Server muss Schreibrechte besitzen.', - 'ok' => 'Berechtigungen des Verzeichnisses ./data/users sind in Ordnung.', + 'ok' => 'Die Berechtigungen des Verzeichnisses ./data/users sind in Ordnung.', ), ), 'conf' => array( '_' => 'Allgemeine Konfiguration', - 'ok' => 'Allgemeine Konfiguration ist gespeichert worden.', + 'ok' => 'Die allgemeine Konfiguration ist gespeichert worden.', ), 'congratulations' => 'Glückwunsch!', 'default_user' => 'Nutzername des Standardbenutzers (maximal 16 alphanumerische Zeichen)', 'delete_articles_after' => 'Entferne Artikel nach', - 'fix_errors_before' => 'Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.', + 'fix_errors_before' => 'Bitte den Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.', 'javascript_is_better' => 'FreshRSS ist ansprechender mit aktiviertem JavaScript', 'language' => array( '_' => 'Sprache', 'choose' => 'Wählen Sie eine Sprache für FreshRSS', - 'defined' => 'Sprache ist festgelegt worden.', + 'defined' => 'Die Sprache ist festgelegt worden.', ), 'not_deleted' => 'Etwas ist schiefgelaufen; Sie müssen die Datei %s manuell löschen.', 'ok' => 'Der Installationsvorgang war erfolgreich.', From 044c4428062ea215c9fe2f46fb47078854048e61 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 19:36:42 +0200 Subject: [PATCH 65/90] i18n: German Completed cherry-pick of https://github.com/FreshRSS/FreshRSS/pull/832 + corrections --- app/i18n/de/admin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php index 8550805ee..c0cbf6787 100644 --- a/app/i18n/de/admin.php +++ b/app/i18n/de/admin.php @@ -127,11 +127,11 @@ return array( 'entry_repartition' => 'Einträge-Verteilung', 'feed' => 'Feed', 'feed_per_category' => 'Feeds pro Kategorie', - 'idle' => 'Inkative Feeds', + 'idle' => 'Inaktive Feeds', 'main' => 'Haupt-Statistiken', 'main_stream' => 'Haupt-Feeds', 'menu' => array( - 'idle' => 'Inkative Feeds', + 'idle' => 'Inaktive Feeds', 'main' => 'Haupt-Statistiken', 'repartition' => 'Artikel-Verteilung', ), From a19e908b73b3fe011bc40609d1edb7f7da6030c8 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 22:04:47 +0200 Subject: [PATCH 66/90] Changelog, readme Several spelling mistakes corrected. Slight rewording of some sentences. Cherry-picked from PubSubHubbub https://github.com/FreshRSS/FreshRSS/pull/831 (it should have been a different branch) https://github.com/Alkarex/FreshRSS/commit/3adab4b70fab858048bd68ed72e71676c4d5badf --- CHANGELOG | 8 ++++++++ README.fr.md | 21 +++++++++------------ README.md | 33 +++++++++++++++------------------ 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d1b49d339..fea4e8871 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,14 @@ ## 2015-xx-xx FreshRSS 1.1.1 (beta) +* Features + * New option to detect and mark updated articles as unread. + * Support for internationalized domain name (IDN). +* Misc. + * Improved logic for automatic deletion of old articles. + * Attempt to better handle encoded titles. + + ## 2015-01-31 FreshRSS 1.0.0 / 1.1.0 (beta) * UI diff --git a/README.fr.md b/README.fr.md index 6c77ccf51..c8f573180 100644 --- a/README.fr.md +++ b/README.fr.md @@ -14,28 +14,25 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) # Note sur les branches -**Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond : +**Ce logiciel est en développement permanent !** Veuillez vous assurer d'utiliser la branche qui vous correspond : * Utilisez [la branche master](https://github.com/FreshRSS/FreshRSS/tree/master/) si vous visez la stabilité. * [La branche beta](https://github.com/FreshRSS/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois. -* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras ! +* Pour les développeurs et ceux qui veulent aider à tester les toutes dernières fonctionnalités, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras ! # Disclaimer -Cette application a été développée pour s’adapter à des besoins personnels et non professionnels. -Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement. -Je m’engage néanmoins à répondre dans la mesure du possible aux demandes d’évolution si celles-ci me semblent justifiées. -Privilégiez pour cela des demandes sur GitHub -(https://github.com/FreshRSS/FreshRSS/issues). +Cette application a été développée pour s’adapter principalement à des besoins personnels, et aucune garantie n'est fournie. +Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues). -# Pré-requis +# Prérequis * Serveur modeste, par exemple sous Linux ou Windows * Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées) * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres) * PHP 5.2.1+ (PHP 5.3.7+ recommandé) - * Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (pour accès API sur platformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés) + * Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés) * Recommandés : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip) * MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ -* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ +* Un navigateur Web récent tel Firefox, Chrome, Opera, Safari. [Internet Explorer ne fonctionne plus, mais ce sera corrigé](https://github.com/FreshRSS/FreshRSS/issues/772). * Fonctionne aussi sur mobile ![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png) @@ -63,7 +60,7 @@ C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web Par exemple, pour exécuter le script toutes les heures : ``` -7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 +7 * * * * php /votre-chemin/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 ``` # Conseils @@ -75,7 +72,7 @@ Par exemple, pour exécuter le script toutes les heures : # Sauvegarde * Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php` et éventuellement `./data/persona/` * Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML -* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL : +* Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL : ```bash mysqldump -u utilisateur -p --databases freshrss > freshrss.sql diff --git a/README.md b/README.md index 089bbd780..830dcc01b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ * [Version française](README.fr.md) # FreshRSS -FreshRSS is a self-hosted RSS feed agregator like [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](http://tontof.net/kriss/feed/). +FreshRSS is a self-hosted RSS feed aggregator such as [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](http://tontof.net/kriss/feed/). -It is at the same time light-weight, easy to work with, powerful and customizable. +It is at the same time lightweight, easy to work with, powerful and customizable. It is a multi-user application with an anonymous reading mode. @@ -14,28 +14,25 @@ It is a multi-user application with an anonymous reading mode. ![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) # Note on branches -**This application is still in development!** Please use the branch that suits your needs: +**This application is under continuous development!** Please use the branch that suits your needs: * Use [the master branch](https://github.com/FreshRSS/FreshRSS/tree/master/) if you need a stable version. * [The beta branch](https://github.com/FreshRSS/FreshRSS/tree/beta) is the default branch: new features are added on a monthly basis. -* For developers and tech savvy persons, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you! +* For developers and tech savvy persons willing to help testing the latest features, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you! # Disclaimer -This application was developed to fulfill personal needs not professional needs. -There is no guarantee neither on its security nor its proper functioning. -If there is feature requests which I think are good for the project, I'll do my best to include them. -The best way is to open issues on GitHub -(https://github.com/FreshRSS/FreshRSS/issues). +This application was developed to fulfil personal needs primarily, and comes with absolutely no warranty. +Feature requests, bug reports, and other contributions are welcome. The best way is to [open issues on GitHub](https://github.com/FreshRSS/FreshRSS/issues). # Requirements * Light server running Linux or Windows * It even works on Raspberry Pi with response time under a second (tested with 150 feeds, 22k articles, or 32Mo of compressed data) -* A web server: Apache2 (recommanded), nginx, lighttpd (not tested on others) -* PHP 5.2.1+ (PHP 5.3.7+ recommanded) +* A web server: Apache2 (recommended), nginx, lighttpd (not tested on others) +* PHP 5.2.1+ (PHP 5.3.7+ recommended) * Required extensions: [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names) - * Recommanded extensions : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip) -* MySQL 5.0.3+ (recommanded) or SQLite 3.7.4+ -* A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ + * Recommended extensions: [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip) +* MySQL 5.0.3+ (recommended) or SQLite 3.7.4+ +* A recent browser like Firefox, Chrome, Opera, Safari. [Internet Explorer currently not supported, but support will come back](https://github.com/FreshRSS/FreshRSS/issues/772). * Works on mobile ![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png) @@ -45,7 +42,7 @@ The best way is to open issues on GitHub 2. Dump the application on your server (expose only the `./p/` folder) 3. Add write access on `./data/` folder to the webserver user 4. Access FreshRSS with your browser and follow the installation process -5. Every thing should be working :) If you encounter any problem, feel free to contact me. +5. Everything should be working :) If you encounter any problem, feel free to contact me. 6. Advanced configuration settings can be seen in [config.php](./data/config.default.php). # Access control @@ -59,18 +56,18 @@ It is needed for the multi-user mode to limit access to FreshRSS. You can: # Automatic feed update * You can add a Cron job to launch the update script. Check the Cron documentation related to your distribution ([Debian/Ubuntu](https://help.ubuntu.com/community/CronHowto), [Red Hat/Fedora](https://fedoraproject.org/wiki/Administration_Guide_Draft/Cron), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](https://wiki.gentoo.org/wiki/Cron), [Arch Linux](https://wiki.archlinux.org/index.php/Cron)…). -It’s a good idea to use the web server user . +It’s a good idea to use the Web server user. For example, if you want to run the script every hour: ``` -7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 +7 * * * * php /your-path/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 ``` # Advices * For a better security, expose only the `./p/` folder on the web. * Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it. * The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here. -* If you encounter any problem, logs are accessibles from the interface or manually in `./data/log/*.log` files. +* If you encounter any problem, logs are accessible from the interface or manually in `./data/log/*.log` files. # Backup * You need to keep `./data/config.php`, `./data/*_user.php` and `./data/persona/` files From 005751d3fb43eefb57b52e8b8cbdf4da1a3578e8 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 22:57:51 +0200 Subject: [PATCH 67/90] jQuery 2.1.4 http://blog.jquery.com/2015/04/28/jquery-1-11-3-and-2-1-4-released-ios-fail-safe-edition/ --- p/scripts/jquery.min.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/p/scripts/jquery.min.js b/p/scripts/jquery.min.js index 25714ed29..49990d6e1 100644 --- a/p/scripts/jquery.min.js +++ b/p/scripts/jquery.min.js @@ -1,4 +1,4 @@ -/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c) -},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("