From a4dac0f791ae6d34b64cee5a3fae1815f6f70a2b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 13 Jun 2014 18:54:26 +0200 Subject: Version 0.7.2 --- README.md | 4 ++-- constants.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ce3c8ef32..172ffd394 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ * Développeur : Marien Fressinaud -* Version actuelle : 0.8-dev -* Date de publication 2014-0x-xx +* Version actuelle : 0.7.2 +* Date de publication 2014-06-13 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/constants.php b/constants.php index e32b8cbc3..b75aa4576 100644 --- a/constants.php +++ b/constants.php @@ -1,5 +1,5 @@ Date: Mon, 21 Jul 2014 18:10:34 +0200 Subject: Version 0.7.3 --- README.md | 4 ++-- constants.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5e6912524..f857bae99 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ * Développeur : Marien Fressinaud -* Version actuelle : 0.7.2 -* Date de publication 2014-06-13 +* Version actuelle : 0.7.3 +* Date de publication 2014-07-21 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/constants.php b/constants.php index b75aa4576..0850219c5 100644 --- a/constants.php +++ b/constants.php @@ -1,5 +1,5 @@ Date: Sat, 9 Aug 2014 00:06:21 +0200 Subject: Correct typo in french translation (missing 's') --- app/i18n/fr.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/fr.php b/app/i18n/fr.php index e04078dba..a3b966c6d 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -224,7 +224,7 @@ return array ( 'persona_connection_email' => 'Adresse courriel de connexion
(pour Mozilla Persona)', 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', - 'unsafe_autologin' => 'Autoriser les connexion automatiques non-sûres au format : ', + 'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ', 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applis mobiles)', 'auth_token' => 'Jeton d’identification', 'explain_token' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.
%s?output=rss&token=%s', -- cgit v1.2.3 From 1d2527b8bc2a125cc8b8508402417f60d314e330 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 9 Aug 2014 12:03:24 +0200 Subject: Revert "Correct typo in french translation (missing 's')" This reverts commit 4beabd52922a1e4769be58c8c7a01bf009b687a4. This PR should not be merged directly in beta! See https://github.com/marienfressinaud/FreshRSS/pull/572 --- app/i18n/fr.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/fr.php b/app/i18n/fr.php index a3b966c6d..e04078dba 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -224,7 +224,7 @@ return array ( 'persona_connection_email' => 'Adresse courriel de connexion
(pour Mozilla Persona)', 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', - 'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ', + 'unsafe_autologin' => 'Autoriser les connexion automatiques non-sûres au format : ', 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applis mobiles)', 'auth_token' => 'Jeton d’identification', 'explain_token' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.
%s?output=rss&token=%s', -- cgit v1.2.3 From 94ad9cf073962d9ff8076ef8e0db35e513d565a7 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 24 Aug 2014 15:29:48 +0200 Subject: Version 0.7.4 --- README.fr.md | 4 ++-- README.md | 4 ++-- constants.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.fr.md b/README.fr.md index df43ef7e8..9c18f5656 100644 --- a/README.fr.md +++ b/README.fr.md @@ -10,8 +10,8 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ * Développeur : Marien Fressinaud -* Version actuelle : 0.8-dev -* Date de publication 2014-0x-xx +* Version actuelle : 0.7.4 +* Date de publication 2014-08-24 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/README.md b/README.md index 0a22df8f1..47beee892 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ It is a multi-user application with an anonymous reading mode. * Official website: http://freshrss.org * Demo: http://demo.freshrss.org/ * Developer: Marien Fressinaud -* Current version: 0.8-dev -* Publication date: 2014-0x-xx +* Current version: 0.7.4 +* Publication date: 2014-08-24 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/constants.php b/constants.php index 0850219c5..b3ff177b2 100644 --- a/constants.php +++ b/constants.php @@ -1,5 +1,5 @@ Date: Fri, 26 Sep 2014 14:47:48 +0200 Subject: Version 0.8.0 --- CHANGELOG | 2 +- README.fr.md | 4 ++-- README.md | 4 ++-- constants.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d7de86d9b..44d3452ae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # Journal des modifications -## 2014-09-xx FreshRSS 0.8.0 +## 2014-09-26 FreshRSS 0.8.0 / 0.9.0 (beta) * UI * New interface for statistics diff --git a/README.fr.md b/README.fr.md index df43ef7e8..46f1e7c8f 100644 --- a/README.fr.md +++ b/README.fr.md @@ -10,8 +10,8 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ * Développeur : Marien Fressinaud -* Version actuelle : 0.8-dev -* Date de publication 2014-0x-xx +* Version actuelle : 0.8.0 +* Date de publication 2014-09-26 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/README.md b/README.md index 0a22df8f1..501dca342 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ It is a multi-user application with an anonymous reading mode. * Official website: http://freshrss.org * Demo: http://demo.freshrss.org/ * Developer: Marien Fressinaud -* Current version: 0.8-dev -* Publication date: 2014-0x-xx +* Current version: 0.8.0 +* Publication date: 2014-09-26 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/constants.php b/constants.php index 4c515d121..464e2da66 100644 --- a/constants.php +++ b/constants.php @@ -1,5 +1,5 @@ Date: Fri, 26 Sep 2014 14:51:53 +0200 Subject: Version 0.9.0 --- CHANGELOG | 2 +- README.fr.md | 4 ++-- README.md | 4 ++-- constants.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d7de86d9b..44d3452ae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # Journal des modifications -## 2014-09-xx FreshRSS 0.8.0 +## 2014-09-26 FreshRSS 0.8.0 / 0.9.0 (beta) * UI * New interface for statistics diff --git a/README.fr.md b/README.fr.md index 9c18f5656..3e15c29fa 100644 --- a/README.fr.md +++ b/README.fr.md @@ -10,8 +10,8 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ * Développeur : Marien Fressinaud -* Version actuelle : 0.7.4 -* Date de publication 2014-08-24 +* Version actuelle : 0.9.0 +* Date de publication 2014-09-26 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/README.md b/README.md index 47beee892..0c2872944 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ It is a multi-user application with an anonymous reading mode. * Official website: http://freshrss.org * Demo: http://demo.freshrss.org/ * Developer: Marien Fressinaud -* Current version: 0.7.4 -* Publication date: 2014-08-24 +* Current version: 0.9.0 +* Publication date: 2014-09-26 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) diff --git a/constants.php b/constants.php index d9f2eab08..5bbb1a6a8 100644 --- a/constants.php +++ b/constants.php @@ -1,5 +1,5 @@ Date: Thu, 2 Oct 2014 10:05:26 +0200 Subject: Fix a bug in main.js In global view, there is no ".category.all>a" element so we tried to apply str2int on an undefined value. In consequence, we were not able to mark several articles as read This patch need to apply on 0.8.1 and 0.9.1 See https://github.com/marienfressinaud/FreshRSS/issues/649 --- p/scripts/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p/scripts/main.js b/p/scripts/main.js index f6d5d2907..9e39010bd 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -33,7 +33,7 @@ function needsScroll($elem) { } function str2int(str) { - if (str == '') { + if (str == '' || str === undefined) { return 0; } return parseInt(str.replace(/\D/g, ''), 10) || 0; -- cgit v1.2.3 From 2fc3068d1b796152f374cfba3528f4273ad37bce Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Tue, 30 Sep 2014 11:08:30 +0200 Subject: Add space after tag icon Fix https://github.com/marienfressinaud/FreshRSS/issues/643 --- app/views/helpers/view/normal_view.phtml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/helpers/view/normal_view.phtml b/app/views/helpers/view/normal_view.phtml index 1dbf14f4c..6d9789f8d 100644 --- a/app/views/helpers/view/normal_view.phtml +++ b/app/views/helpers/view/normal_view.phtml @@ -153,14 +153,15 @@ if (!empty($this->entries)) { if (!empty($tags)) { ?>
  • + + -- cgit v1.2.3 From e7c36d515286079491c5dd6d6db7cc87d47dd043 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 27 Sep 2014 08:20:13 -0400 Subject: Fix read all shortcut Before, the read all shortcut raised a javascript exception when we use the confirmation option. The confirmation was not even displayed. The code was failling since there was a missing variable. Now, the shortcut ask for confirmation when the option is selected in the reading configuration page. The definition of the shortcut was buggy since the confirmation was handled properly in an other location. I simplified the code by removing the confirmation code in the shortcut. --- p/scripts/main.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/p/scripts/main.js b/p/scripts/main.js index 9e39010bd..00cd96fbe 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -538,14 +538,7 @@ function init_shortcuts() { }); shortcut.add("shift+" + shortcuts.mark_read, function () { // on marque tout comme lu - var btn = $(".nav_menu .read_all"); - if (btn.hasClass('confirm')) { - if (confirm(str_confirmation)) { - btn.click(); - } - } else { - btn.click(); - } + $(".nav_menu .read_all").click(); }, { 'disable_in_input': true }); -- cgit v1.2.3 From d6396be41ad140fb61f9d31a6a5d51aab427cc65 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 2 Oct 2014 22:02:09 +0200 Subject: Fix category not appear on feed.add page (GET) Must apply to 0.8.1 and 0.9.1 See https://github.com/marienfressinaud/FreshRSS/issues/649 --- 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 c7cc25fbb..f75c969d9 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -171,7 +171,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // GET request so we must ask confirmation to user Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · '); - $this->view->categories = $this->catDAO->listCategories(); + $this->view->categories = $this->catDAO->listCategories(false); $this->view->feed = new FreshRSS_Feed($url); try { // We try to get some more information about the feed -- cgit v1.2.3 From 0965aecefef0fa1e67e649d24c6d4b3ba0e143c1 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Mon, 29 Sep 2014 18:57:40 -0400 Subject: Add string delimiters for averages. Before, if the average was equal to 0, it was displayed on the graph but the label was not displayed. Now, the label is displayed. Conflicts: app/views/stats/index.phtml --- app/views/stats/index.phtml | 9 ++++++++- app/views/stats/repartition.phtml | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/views/stats/index.phtml b/app/views/stats/index.phtml index 412e77e16..bbdcfaec8 100644 --- a/app/views/stats/index.phtml +++ b/app/views/stats/index.phtml @@ -92,7 +92,14 @@ function initStats() { } // Entry per day Flotr.draw(document.getElementById('statsEntryPerDay'), - [count ?>], + [{ + data: count ?>, + bars: {horizontal: false, show: true} + },{ + data: avg, + lines: {show: true}, + label: "average?>" + }], { grid: {verticalLines: false}, bars: {horizontal: false, show: true}, diff --git a/app/views/stats/repartition.phtml b/app/views/stats/repartition.phtml index b425c1458..750a3ffdc 100644 --- a/app/views/stats/repartition.phtml +++ b/app/views/stats/repartition.phtml @@ -67,7 +67,7 @@ function initStats() { }, { data: avg_h, lines: {show: true}, - label: averageHour?>, + label: "averageHour?>", yaxis: 2 }], { @@ -96,7 +96,7 @@ function initStats() { }, { data: avg_dow, lines: {show: true}, - label: averageDayOfWeek?>, + label: "averageDayOfWeek?>", yaxis: 2 }], { @@ -126,7 +126,7 @@ function initStats() { }, { data: avg_m, lines: {show: true}, - label: averageMonth?>, + label: "averageMonth?>", yaxis: 2 }], { -- cgit v1.2.3 From 57d4914bf81f3380fe55fa06b7b312f1c463e3e3 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Mon, 29 Sep 2014 18:54:03 -0400 Subject: Add an average per day for the 30 day period Conflicts: app/views/stats/index.phtml --- app/Controllers/statsController.php | 1 + app/Models/StatsDAO.php | 27 ++++++++++++++++++++++++--- app/Models/StatsDAOSQLite.php | 23 +++++++++++++++++++++++ app/views/stats/index.phtml | 7 +++++-- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 256543f37..3069be34d 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -21,6 +21,7 @@ class FreshRSS_stats_Controller extends Minz_ActionController { Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js'))); $this->view->repartition = $statsDAO->calculateEntryRepartition(); $this->view->count = $statsDAO->calculateEntryCount(); + $this->view->average = $statsDAO->calculateEntryAverage(); $this->view->feedByCategory = $statsDAO->calculateFeedByCategory(); $this->view->entryByCategory = $statsDAO->calculateEntryByCategory(); $this->view->topFeed = $statsDAO->calculateTopFeed(); diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php index 40505ab3e..08dd4cd5c 100644 --- a/app/Models/StatsDAO.php +++ b/app/Models/StatsDAO.php @@ -79,6 +79,27 @@ SQL; return $this->convertToSerie($count); } + /** + * Calculates entry average per day on a 30 days period. + * + * @return integer + */ + public function calculateEntryAverage() { + $period = self::ENTRY_COUNT_PERIOD; + + // Get stats per day for the last 30 days + $sql = <<prefix}entry AS e +WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d') +SQL; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetch(PDO::FETCH_NAMED); + + return round($res['average'], 2); + } + /** * Initialize an array for the entry count. * @@ -160,7 +181,7 @@ SQL; public function calculateEntryAveragePerFeedPerHour($feed = null) { return $this->calculateEntryAveragePerFeedPerPeriod(1/24, $feed); } - + /** * Calculates the average number of article per day of week per feed * @@ -180,10 +201,10 @@ SQL; public function calculateEntryAveragePerFeedPerMonth($feed = null) { return $this->calculateEntryAveragePerFeedPerPeriod(30, $feed); } - + /** * Calculates the average number of article per feed - * + * * @param float $period number used to divide the number of day in the period * @param integer $feed id * @return integer diff --git a/app/Models/StatsDAOSQLite.php b/app/Models/StatsDAOSQLite.php index 3b1256de1..bb2336532 100644 --- a/app/Models/StatsDAOSQLite.php +++ b/app/Models/StatsDAOSQLite.php @@ -34,6 +34,29 @@ SQL; return $this->convertToSerie($count); } + /** + * Calculates entry average per day on a 30 days period. + * + * @return integer + */ + public function calculateEntryAverage() { + $period = self::ENTRY_COUNT_PERIOD; + + // Get stats per day for the last 30 days + $sql = <<prefix}entry AS e +WHERE strftime('%Y%m%d', e.date, 'unixepoch') + BETWEEN strftime('%Y%m%d', 'now', '-{$period} days') + AND strftime('%Y%m%d', 'now', '-1 day') +SQL; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetch(PDO::FETCH_NAMED); + + return round($res['average'], 2); + } + protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) { if ($feed) { $restrict = "WHERE e.id_feed = {$feed}"; diff --git a/app/views/stats/index.phtml b/app/views/stats/index.phtml index bbdcfaec8..37803f2f9 100644 --- a/app/views/stats/index.phtml +++ b/app/views/stats/index.phtml @@ -91,6 +91,10 @@ function initStats() { return; } // Entry per day + var avg = []; + for (var i = -31; i <= 0; i++) { + avg.push([i, average?>]); + } Flotr.draw(document.getElementById('statsEntryPerDay'), [{ data: count ?>, @@ -102,8 +106,7 @@ function initStats() { }], { grid: {verticalLines: false}, - bars: {horizontal: false, show: true}, - xaxis: {noTicks: 6, showLabels: false, tickDecimals: 0}, + xaxis: {noTicks: 6, showLabels: false, tickDecimals: 0, min: -30.75, max: -0.25}, yaxis: {min: 0}, mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}} }); -- cgit v1.2.3 From 8a09958281e7a4352c86d5734cbc9c6dc90b3c96 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Mon, 29 Sep 2014 18:26:28 -0400 Subject: Add percent of total on top 10 feeds --- app/i18n/en.php | 1 + app/i18n/fr.php | 1 + app/views/stats/index.phtml | 2 ++ 3 files changed, 4 insertions(+) diff --git a/app/i18n/en.php b/app/i18n/en.php index beba02c4d..0d3654744 100644 --- a/app/i18n/en.php +++ b/app/i18n/en.php @@ -56,6 +56,7 @@ return array ( 'stats_entry_per_hour' => 'Per hour', 'stats_entry_per_day_of_week' => 'Per day of week', 'stats_entry_per_month' => 'Per month', + 'stats_percent_of_total' => '%% of total', 'last_week' => 'Last week', 'last_month' => 'Last month', diff --git a/app/i18n/fr.php b/app/i18n/fr.php index b0fbf15ae..c72fc3e93 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -56,6 +56,7 @@ return array ( 'stats_entry_per_hour' => 'Par heure', 'stats_entry_per_day_of_week' => 'Par jour de la semaine', 'stats_entry_per_month' => 'Par mois', + 'stats_percent_of_total' => '%% du total', 'last_week' => 'Depuis la semaine dernière', 'last_month' => 'Depuis le mois dernier', diff --git a/app/views/stats/index.phtml b/app/views/stats/index.phtml index 37803f2f9..fa57a77c0 100644 --- a/app/views/stats/index.phtml +++ b/app/views/stats/index.phtml @@ -48,6 +48,7 @@ + @@ -56,6 +57,7 @@ + repartition['all_feeds']['total'] * 100, 1);?> -- cgit v1.2.3 From d842f85966b2873f1074c73ad79ec5169e7090f2 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 5 Oct 2014 16:53:43 +0200 Subject: SimplePie enclosure bug workaround https://github.com/marienfressinaud/FreshRSS/issues/504 --- app/Models/Feed.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 2a5ea45ac..03baf3ad2 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -288,6 +288,8 @@ class FreshRSS_Feed extends Minz_Model { $content .= '
  • diff --git a/p/themes/base-theme/template.css b/p/themes/base-theme/template.css index aab511fb9..80f76e80b 100644 --- a/p/themes/base-theme/template.css +++ b/p/themes/base-theme/template.css @@ -692,6 +692,7 @@ br + br + br { position: relative; min-width: 260px; max-width: 640px; + margin-bottom: 30px; } .slides * { user-select: none; @@ -740,26 +741,19 @@ br + br + br { padding: 0; } .properties { - bottom: 15px; + bottom: -35px; + position: absolute; + width: 100%; display: none; - left: 15%; - padding: 5px; +} +.properties .page-number { + right: 0; + top: 0; position: absolute; - text-align: center; - width: 70%; - font-size: 15pt; - text-shadow: 0px 0px 15px rgb(119, 119, 119); - border-radius: 5px; - box-shadow: 5px 5px 10px; - border: 1px solid; - background-color: #fff; } .slide:hover + .nav label { opacity: 0.5; } -.slide:hover ~ .properties { - display: block; -} .nav label:hover { opacity: 1; } @@ -774,6 +768,9 @@ input:checked + .slide-container .slide { input:checked + .slide-container .nav label { display: block; } +input:checked + .slide-container .properties { + display: block; +} /*=== DIVERS */ /*===========*/ -- cgit v1.2.3 From a3a77b09e96f66eec4e31e638b58c785cfb369a3 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 22 Nov 2014 08:01:31 -0500 Subject: Add an automatic sticky post configuration Before, when the article while marked as read while scrolling and auto removed after reading, the display was in the middle of the following article. Now, the article is forced to be a sticky article so the user display is forced to be on top of the following article. --- app/Models/Context.php | 18 ++++++++++++++++++ app/views/helpers/javascript_vars.phtml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/Models/Context.php b/app/Models/Context.php index cbd6a5888..3dc5349ad 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -287,4 +287,22 @@ class FreshRSS_Context { } return true; } + + /** + * Determine if the "sticky post" option is enabled. It can be enable + * by the user when it is selected in the configuration page or by the + * application when the context allows to auto-remove articles when they + * are read. + * + * @return boolean + */ + public static function isStickyPostEnabled() { + if (self::$conf->sticky_post) { + return true; + } + if (self::isAutoRemoveAvailable()) { + return true; + } + return false; + } } diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml index 6424d8d04..0961ac3fe 100644 --- a/app/views/helpers/javascript_vars.phtml +++ b/app/views/helpers/javascript_vars.phtml @@ -27,7 +27,7 @@ echo 'var context={', 'auto_load_more:', FreshRSS_Context::$conf->auto_load_more ? 'true' : 'false', ',', 'auto_actualize_feeds:', $auto_actualize ? 'true' : 'false', ',', 'does_lazyload:', FreshRSS_Context::$conf->lazyload ? 'true' : 'false', ',', - 'sticky_post:', FreshRSS_Context::$conf->sticky_post ? 'true' : 'false', ',', + 'sticky_post:', FreshRSS_Context::isStickyPostEnabled() ? 'true' : 'false', ',', 'html5_notif_timeout:', FreshRSS_Context::$conf->html5_notif_timeout, ',', 'auth_type:"', Minz_Configuration::authType(), '",', 'current_user_mail:', $mail ? ('"' . $mail . '"') : 'null', ',', -- cgit v1.2.3 From 7781add6dab45e890bb873e7fd42f83a6227e62c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 24 Nov 2014 15:45:47 +0100 Subject: Update Screwdriver theme --- p/themes/Screwdriver/icons/favicon-16-32-48-64.ico | Bin 0 -> 1150 bytes p/themes/Screwdriver/icons/favicon-256.png | Bin 0 -> 15724 bytes p/themes/Screwdriver/icons/favicon.svg | 280 +++++++++++++++++++++ p/themes/Screwdriver/icons/icon.svg | 271 ++++++++++++++++++++ p/themes/Screwdriver/screwdriver.css | 105 ++++---- 5 files changed, 610 insertions(+), 46 deletions(-) create mode 100644 p/themes/Screwdriver/icons/favicon-16-32-48-64.ico create mode 100644 p/themes/Screwdriver/icons/favicon-256.png create mode 100644 p/themes/Screwdriver/icons/favicon.svg create mode 100644 p/themes/Screwdriver/icons/icon.svg diff --git a/p/themes/Screwdriver/icons/favicon-16-32-48-64.ico b/p/themes/Screwdriver/icons/favicon-16-32-48-64.ico new file mode 100644 index 000000000..7c5eb5517 Binary files /dev/null and b/p/themes/Screwdriver/icons/favicon-16-32-48-64.ico differ diff --git a/p/themes/Screwdriver/icons/favicon-256.png b/p/themes/Screwdriver/icons/favicon-256.png new file mode 100644 index 000000000..235b18c97 Binary files /dev/null and b/p/themes/Screwdriver/icons/favicon-256.png differ diff --git a/p/themes/Screwdriver/icons/favicon.svg b/p/themes/Screwdriver/icons/favicon.svg new file mode 100644 index 000000000..aaab5de99 --- /dev/null +++ b/p/themes/Screwdriver/icons/favicon.svg @@ -0,0 +1,280 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/p/themes/Screwdriver/icons/icon.svg b/p/themes/Screwdriver/icons/icon.svg new file mode 100644 index 000000000..7f3d76af2 --- /dev/null +++ b/p/themes/Screwdriver/icons/icon.svg @@ -0,0 +1,271 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/p/themes/Screwdriver/screwdriver.css b/p/themes/Screwdriver/screwdriver.css index b6c2e670e..8dbd2ec34 100644 --- a/p/themes/Screwdriver/screwdriver.css +++ b/p/themes/Screwdriver/screwdriver.css @@ -52,7 +52,7 @@ input, select, textarea { color: #222; line-height: 25px; vertical-align: middle; - box-shadow: 0 2px 2px #eee inset, 0 1px #fff; + box-shadow: 0 1px 2px #ccc inset, 0 1px #fff; } option { padding: 0 .5em; @@ -121,30 +121,14 @@ form th { } /*=== Buttons */ -form#add_rss .stick input, .dropdown-menu select{ - background:#393939; - box-shadow: 0 2px 2px #171717 inset,0 1px rgba(255,255,255,0.08); - border-left:solid 1px #171717; - border-top:solid 1px #171717; - border-bottom:solid 1px #171717; - border-right:none; + +.dropdown-menu .input select, .dropdown-menu .input input { + background:#444; color:#fff; + box-shadow:0 2px 2px #222 inset, 0px 1px rgba(255, 255, 255, 0.08); + border:solid 1px #171717; } -form#add_rss .stick .btn{ - background:linear-gradient(180deg, #222 0%, #171717 100%) #171717; - background: -webkit-linear-gradient(top, #222 0%, #171717 100%); - box-shadow:0 1px rgba(255,255,255,0.08), 0px 1px rgba(255, 255, 255, 0.08) inset; - border-right:solid 1px #171717; - border-top:solid 1px #171717; - border-bottom:solid 1px #171717; - border-left:none; -} -form#add_rss .stick .btn.dropdown-toggle{ - border-right:solid 1px #171717; - border-top:solid 1px #171717; - border-bottom:solid 1px #171717; - border-left:solid 1px #171717; -} + .stick { vertical-align: middle; font-size: 0; @@ -206,14 +190,13 @@ a.btn { background: linear-gradient(180deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; background: -webkit-linear-gradient(top, #EDE7DE 0%, #FFF 100%); } -#loginButton.btn{ - border:none; - box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08); -} + .nav_menu .btn.active, .nav_menu .btn:active, .nav_menu .dropdown-target:target ~ .btn.dropdown-toggle{ - box-shadow: 0px 2px #E2972A; - border-radius: 0; - background:transparent; + box-shadow: 0 1px #fff; + border-radius: 4px; + background: linear-gradient(180deg, #EDE7DE 0%, #F6F6F6 100%) #EDE7DE; + background: -webkit-linear-gradient(top, #EDE7DE 0%, #F6F6F6 100%); + border: solid 1px #ccc; } .nav_menu .btn { border: 0; @@ -500,7 +483,8 @@ a.btn { /*=== Boxes */ .box { background: #EDE7DE; - border-radius: 4px 4px 0 0; + border-radius: 4px; + box-shadow: 0 1px #fff; } .box .box-title { margin: 0; @@ -575,6 +559,7 @@ a.btn { .tree-folder-items > .item > a { text-decoration: none; color: #fff; + font-size: 0.92em; } .tree-folder-items > .item.active > a { } @@ -593,7 +578,8 @@ a.btn { text-align: center; } .header > .item.title .logo { - display: none; + height: 60px; + width: 60px; } .header > .item.title{ width: 250px; @@ -603,9 +589,9 @@ a.btn { } .header > .item.title h1 a { text-decoration: none; - font-size: 38px; - color:#ccc; - text-shadow: 0 1px #fff, 0 -1px rgba(162, 162, 162, 1); + font-size: 28px; + color:#222; + text-shadow: 0 1px #fff; } .header > .item.search input { width: 230px; @@ -681,11 +667,8 @@ a.btn { /*=== Prompt (centered) */ .prompt { text-align: center; - color: #FFF; - background: #222; padding: 14px 0px; - box-shadow: 0px -1px #FFF, 0px 1px #FFF, 0px 2px 2px #171717 inset, 0px -2px 2px #171717 inset; - text-shadow: 0 -1px #171717, 0 1px rgba(255,255,255,0.08); + text-shadow: 0 1px rgba(255,255,255,0.08); } .prompt label { text-align: left; @@ -702,9 +685,9 @@ a.btn { margin: 20px 0; } .prompt input#username,.prompt input#passwordPlain{ - border:none; - box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08); - background:#EDE7DE; + border:solid 1px #ccc; + box-shadow: 0 4px -4px #ccc inset,0px 1px rgba(255, 255, 255, 0.08); + background:#fff; } .prompt input#username:focus,.prompt input#passwordPlain:focus{ border: solid 1px #E7AB34; @@ -734,7 +717,11 @@ a.btn { padding: 0 10px; font-style:italic; line-height: 3em; - background: #fff; + box-shadow: 0 1px #BDB7AE inset, 0 -1px rgba(255,255,255,0.28) inset; + background: linear-gradient(0deg, #EDE7DE 0%, #C2BCB3 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #C2BCB3 0%, #FFF 100%); + color: #666; + text-shadow: 0 1px rgba(255,255,255,0.28); text-align: center; } #new-article + .day { @@ -748,10 +735,26 @@ a.btn { .nav_menu { background: #EDE7DE; border-bottom: 1px solid #ccc; - box-shadow:0 -1px #fff inset; + box-shadow:0 -1px rgba(255, 255, 255, 0.28) inset; text-align: center; padding: 5px 0; } +#panel >.nav_menu{ + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); +} +#panel > .nav_menu > #nav_menu_read_all{ + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); + border-radius: 4px; + border: 1px solid #CCC; + box-shadow: 0px 1px #FFF; +} +#panel > .nav_menu > #nav_menu_read_all > .dropdown > .btn.dropdown-toggle{ + border-radius: 0 4px 4px 0; + border:none; + border-left: solid 1px #ccc; +} /*=== Feed articles */ .flux_content { @@ -810,6 +813,7 @@ opacity: 1; border-top: 1px solid #ddd; font-size: 0.8rem; cursor: pointer; + box-shadow: 0 -1px rgba(255,255,255,0.28) inset; } .flux_header .title { font-size: 0.9rem; @@ -917,6 +921,7 @@ opacity: 1; text-shadow: 0 -1px 0 #aaa; color: #666; background: #EDE7DE; + box-shadow: 0 1px rgba(255,255,255,0.28)inset; } #bigMarkAsRead:hover { color: #000; @@ -956,9 +961,8 @@ opacity: 1; /*=== GLOBAL VIEW */ /*================*/ #stream.global { - background: #222; padding: 24px 0; - box-shadow: 0 1px #fff, 0 -2px 2px #171717 inset, 0 2px 2px #171717 inset; + box-shadow: 0px 8px 8px #C2BCB3 inset; } .box.category .box-title { @@ -1004,6 +1008,7 @@ opacity: 1; #panel { box-shadow: 0px 0px 4px #000; border-radius: 8px; + background:#EDE7DE; } /*=== DIVERS */ /*===========*/ @@ -1084,6 +1089,14 @@ opacity: 1; color: #eee; } +#slider.active { + box-shadow: -4px 0 4px rgba(15, 15, 15, 0.55); + background: #F8F8F8; +} +#close-slider.active { + background: rgba(15, 15, 15, 0.35); +} + /*=== MOBILE */ /*===========*/ @media screen and (max-width: 840px) { -- cgit v1.2.3 From b9b3e963b49b255dd694b001b303e75f58519b67 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 24 Nov 2014 15:49:13 +0100 Subject: Add BlueLagoon theme --- p/themes/BlueLagoon/BlueLagoon.css | 1210 +++++++++++++++++++++ p/themes/BlueLagoon/README.md | 38 + p/themes/BlueLagoon/icons/bookmark.svg | 61 ++ p/themes/BlueLagoon/icons/favicon-16-32-48-64.ico | Bin 0 -> 1150 bytes p/themes/BlueLagoon/icons/favicon-256.png | Bin 0 -> 16517 bytes p/themes/BlueLagoon/icons/favicon.svg | 273 +++++ p/themes/BlueLagoon/icons/icon.svg | 291 +++++ p/themes/BlueLagoon/icons/non-starred.svg | 59 + p/themes/BlueLagoon/icons/read.svg | 72 ++ p/themes/BlueLagoon/icons/starred.svg | 60 + p/themes/BlueLagoon/icons/unread.svg | 65 ++ p/themes/BlueLagoon/loader.gif | Bin 0 -> 3164 bytes p/themes/BlueLagoon/metadata.json | 7 + p/themes/BlueLagoon/template.css | 695 ++++++++++++ p/themes/BlueLagoon/thumbs/original.png | Bin 0 -> 153829 bytes 15 files changed, 2831 insertions(+) create mode 100644 p/themes/BlueLagoon/BlueLagoon.css create mode 100644 p/themes/BlueLagoon/README.md create mode 100644 p/themes/BlueLagoon/icons/bookmark.svg create mode 100644 p/themes/BlueLagoon/icons/favicon-16-32-48-64.ico create mode 100644 p/themes/BlueLagoon/icons/favicon-256.png create mode 100644 p/themes/BlueLagoon/icons/favicon.svg create mode 100644 p/themes/BlueLagoon/icons/icon.svg create mode 100644 p/themes/BlueLagoon/icons/non-starred.svg create mode 100644 p/themes/BlueLagoon/icons/read.svg create mode 100644 p/themes/BlueLagoon/icons/starred.svg create mode 100644 p/themes/BlueLagoon/icons/unread.svg create mode 100644 p/themes/BlueLagoon/loader.gif create mode 100644 p/themes/BlueLagoon/metadata.json create mode 100644 p/themes/BlueLagoon/template.css create mode 100644 p/themes/BlueLagoon/thumbs/original.png diff --git a/p/themes/BlueLagoon/BlueLagoon.css b/p/themes/BlueLagoon/BlueLagoon.css new file mode 100644 index 000000000..ae1c24604 --- /dev/null +++ b/p/themes/BlueLagoon/BlueLagoon.css @@ -0,0 +1,1210 @@ +@charset "UTF-8"; + +/*=== FONTS */ +@font-face { + font-family: "OpenSans"; + src: url("../fonts/openSans.woff") format("woff"); +} + +/*=== GENERAL */ +/*============*/ +html, body { + height: 100%; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + background: #fafafa; + font-size: 92%; +} + +/*=== Links */ +a, button.as-link { + color: #0062BE; + outline: none; +} + +/*=== Forms */ +.form-group{ + width: 100%; + float: left; + height: auto; + display: inline-block; +} +legend { + margin: 20px 0 5px; + padding: 5px 0; + border-bottom: 1px solid #ddd; + font-size: 1.4em; +} +label { + min-height: 25px; + padding: 5px 0; + cursor: pointer; +} +textarea { + width: 360px; + height: 100px; +} +input, select, textarea { + min-height: 25px; + padding: 5px; + background: #fff; + border: 1px solid #ccc; + border-radius: 3px; + color: #222; + line-height: 25px; + vertical-align: middle; + box-shadow: 0 1px 2px #ccc inset, 0 1px #fff; +} +option { + padding: 0 .5em; +} +input:focus, select:focus, textarea:focus { + color: #0F0F0F; + box-shadow: 0 0 3px #0062BF; + border: solid 1px #0062BF; +} +input:invalid, select:invalid { + border-color: #f00; + box-shadow: 0 0 2px 2px #fdd inset; +} +input:disabled, select:disabled { + background: #eee; +} +input.extend { + transition: width 200ms linear; + -moz-transition: width 200ms linear; + -webkit-transition: width 200ms linear; + -o-transition: width 200ms linear; + -ms-transition: width 200ms linear; +} + +/*=== Tables */ +table { + border-collapse: collapse; +} + +tr, th, td { + padding: 0.5em; + border: 1px solid #ddd; +} +th { + background: #f6f6f6; +} +form td, +form th { + font-weight: normal; + text-align: center; +} + +/*=== COMPONENTS */ +/*===============*/ +/*=== Forms */ +.form-group.form-actions { + padding: 5px 0; + background: #f4f4f4; + border-top: 1px solid #ddd; +} +.form-group.form-actions .btn { + margin: 0 10px; + border-radius: 4px; + box-shadow:0 1px rgba(255,255,255,0.08) inset; +} +.form-group .group-name { + padding: 10px 0; + text-align: right; +} +.form-group .group-controls { + min-height: 25px; + padding: 5px 0; +} +.form-group table { + margin: 10px 0 0 220px; +} + +/*=== Buttons */ + +.dropdown-menu .input select, .dropdown-menu .input input { + background:#444; + color:#fff; + box-shadow:0 2px 2px #222 inset, 0px 1px rgba(255, 255, 255, 0.08); + border:solid 1px #171717; +} + +.stick { + vertical-align: middle; + font-size: 0; +} +.stick input, +.stick .btn { + border-radius: 0; +} +.stick .btn:first-child,.stick input:first-child { + border-radius: 6px 0 0 6px; +} +.stick .btn-important:first-child { +} +.stick .btn:last-child, .stick input:last-child { + border-radius: 0 6px 6px 0; +} +.stick .btn + .btn, +.stick .btn + input, +.stick .btn + .dropdown > .btn, +.stick input + .btn, +.stick input + input, +.stick input + .dropdown > .btn, +.stick .dropdown + .btn, +.stick .dropdown + input, +.stick .dropdown + .dropdown > .btn { + border-left: none; +} +.stick .btn + .dropdown > .btn { + border-left: none; + border-radius: 0 3px 3px 0; +} + +.btn { + display: inline-block; + min-height: 37px; + min-width: 15px; + margin: 0; + padding: 5px 10px; + color:#222; + border: solid 1px #ccc; + border-radius: 4px; + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); + text-shadow: 0px -1px rgba(255,255,255,0.08); + font-size: 0.9rem; + vertical-align: middle; + cursor: pointer; + overflow: hidden; +} +a.btn { + min-height: 25px; + line-height: 25px; +} +.btn:hover { + text-shadow: 0 0 2px #fff; + text-decoration:none; +} +.btn.active,.btn:active,.dropdown-target:target ~ .btn.dropdown-toggle { + background: linear-gradient(180deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(top, #EDE7DE 0%, #FFF 100%); +} + +.nav_menu .btn.active, .nav_menu .btn:active, .nav_menu .dropdown-target:target ~ .btn.dropdown-toggle{ + box-shadow: 0 1px #fff; + border-radius: 4px; + background: linear-gradient(180deg, #EDE7DE 0%, #F6F6F6 100%) #EDE7DE; + background: -webkit-linear-gradient(top, #EDE7DE 0%, #F6F6F6 100%); + border: solid 1px #ccc; +} +.nav_menu .btn { + border: 0; + background:transparent; +} + +.read_all { + color:#222; +} +.btn.dropdown-toggle[href="#dropdown-configure"]{ + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); + border-radius: 4px; + border: solid 1px #ccc; + box-shadow: 0 1px #fff; +} +.btn.dropdown-toggle:active { + background:transparent; +} +.btn-important { + background: linear-gradient(180deg, #0090FF 0%, #0062BE 100%) #E4992C; + background: -webkit-linear-gradient(top, #0090FF 0%, #0062BE 100%); + color: #FFF; + box-shadow: 0 1px rgba(255,255,255,0.08) inset; + border-radius: 4px; + text-shadow: 0px -1px rgba(255,255,255,0.08); + font-weight: normal; +} +.btn-important:hover { +} +.btn-important:active { + background: linear-gradient(0deg, #E4992C 0%, #D18114 100%) #E4992C; + background: -webkit-linear-gradient(bottom, #E4992C 0%, #D18114 100%); +} + +.btn-attention { + background: #E95B57; + background: linear-gradient(to bottom, #E95B57, #BD362F); + background: -webkit-linear-gradient(top, #E95B57 0%, #BD362F 100%); + color: #fff; + border: 1px solid #C44742; + text-shadow: 0px -1px 0px #666; +} +.btn-attention:hover { + background: linear-gradient(to bottom, #D14641, #BD362F); + background: -webkit-linear-gradient(top, #D14641 0%, #BD362F 100%); +} +.btn-attention:active { + background: #BD362F; + box-shadow: none; +} +.btn[type="reset"]{ + color: #fff; + background:linear-gradient(180deg, #222 0%, #171717 100%) #171717; + background: -webkit-linear-gradient(top, #222 0%, #171717 100%); + box-shadow:0 -1px rgba(255,255,255,0.08) inset; +} +/*=== Navigation */ +.nav-list .nav-header, +.nav-list .item { + height: 2.5em; + line-height: 2.5em; + font-size: 0.9rem; +} +.nav-list .item:hover { + text-shadow: 0 0 2px rgba(255,255,255,0.28); + color:#fff; +} + +.nav-list .item.active { + background: linear-gradient(180deg, #0090FF 0%, #0062BE 100%) #E4992C; + background: -webkit-linear-gradient(top, #0090FF 0%, #0062BE 100%); + border-width: medium medium 1px; + border-style: none none solid; + border-color: -moz-use-text-color -moz-use-text-color #171717; + box-shadow: -1px 2px 2px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset; + margin: 0; +} +.nav-list .item.active a { + color: #fff; +} +.nav-list .disable { + color: #aaa; + background: #fafafa; + text-align: center; +} +.nav-list .item > a { + padding: 0 10px; + color:#ccc; +} +.nav-list a:hover { + text-decoration: none; +} +.nav-list .item.empty a { + color: #f39c12; +} +.nav-list .item.active.empty a { + color: #fff; + background: linear-gradient(180deg, #E4992C 0%, #D18114 100%) #E4992C; + background: -webkit-linear-gradient(180deg, #E4992C 0%, #D18114 100%); +} +.nav-list .item.error a { + color: #BD362F; +} +.nav-list .item.active.error a { + color: #fff; + background: #BD362F; +} + +.nav-list .nav-header { + padding: 0 10px; + color: #222; + background: transparent; +} + +.nav-list .nav-form { + padding: 3px; + text-align: center; +} + +.nav-head { + margin: 0; + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); + text-align: right; +} +.nav-head .item { + padding: 5px 10px; + font-size: 0.9rem; + line-height: 1.5rem; +} + +/*=== Horizontal-list */ +.horizontal-list { + margin: 0; + padding: 0; +} +.horizontal-list .item { + vertical-align: middle; +} + +/*=== Dropdown */ +.dropdown-menu { + margin: 5px 0 0; + padding: 5px 0; + border: 1px solid #171717; + border-radius: 4px; + box-shadow: 0 0 3px #000; + font-size: 0.8rem; + text-align: left; + background: #222; +} +.dropdown-menu:after { + content: ""; + position: absolute; + top: -6px; + right: 13px; + width: 10px; + height: 10px; + background: #222; + border-top: 1px solid #171717; + border-left: 1px solid #171717; + z-index: -10; + transform: rotate(45deg); + -moz-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); +} +.dropdown-header { + display:none; +} +.dropdown-menu > .item > a { + padding: 0 25px; + line-height: 2.5em; + color: #ccc; +} +.dropdown-menu > .item > span, +.dropdown-menu > .item > .as-link { + padding: 0 22px; + line-height: 2em; + color: #ccc; +} +.dropdown-menu > .item:hover { + background: linear-gradient(180deg, #0090FF 0%, #0062BE 100%) #E4992C; + background: -webkit-linear-gradient(top, #0090FF 0%, #0062BE 100%); + color: #fff; +} +.dropdown-menu > .item[aria-checked="true"] > a:before { + font-weight: bold; + margin: 0 0 0 -14px; +} +.dropdown-menu > .item:hover > a { + color: #fff; + text-decoration: none; +} +.dropdown-menu .input select, +.dropdown-menu .input input { + margin: 0 auto 5px; + padding: 2px 5px; + border-radius: 3px; +} + +.separator { + margin: 5px 0; + border-bottom: 1px solid #171717; + box-shadow: 0 1px rgba(255,255,255,0.08); +} + +/*=== Alerts */ +.alert { + margin: 15px auto; + padding: 10px 15px; + background: #f4f4f4; + border: 1px solid #ccc; + border-right: 1px solid #aaa; + border-bottom: 1px solid #aaa; + border-radius: 5px; + color: #aaa; + text-shadow: 0 0 1px #eee; + font-size: 0.9em; +} +.alert-head { + font-size: 1.15em; +} +.alert > a { + color: inherit; + text-decoration: underline; +} +.alert-warn { + background: #ffe; + border: 1px solid #eeb; + color: #c95; +} +.alert-success { + background: #dfd; + border: 1px solid #cec; + color: #484; +} +.alert-error { + background: #fdd; + border: 1px solid #ecc; + color: #844; +} + +/*=== Pagination */ +.pagination { + background: #fafafa; + text-align: center; + color: #333; + font-size: 0.8em; +} +.content .pagination { + margin: 0; + padding: 0; +} +.pagination .item.pager-current { + font-weight: bold; + font-size: 1.5em; +} +.pagination .item a { + display: block; + color: #333; + font-style: italic; + line-height: 3em; + text-decoration: none; +} +.pagination .item a:hover { + background: #ddd; +} +.pagination:first-child .item { + border-bottom: 1px solid #aaa; +} +.pagination:last-child .item { + border-top: 1px solid #ddd; +} + +.pagination .loading, +.pagination a:hover.loading { + background: url("loader.gif") center center no-repeat #fff; + font-size: 0; + height:55px +} + +/*=== Boxes */ +.box { + background: #F9F7F4; + border-radius: 4px; + box-shadow: 0 1px #fff; +} +.box .box-title { + margin: 0; + padding: 5px 10px; + background: linear-gradient(0deg, #EDE7DE 0%, #fff 100%) #171717; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #fff 100%); + box-shadow: 0px -1px #fff inset,0 -2px #ccc inset; + color: #888; + text-shadow: 0 1px #ccc; + border-radius: 4px 4px 0 0; + font-size: 1.1rem; + font-weight: normal; +} +.box .box-content { + max-height: 260px; +} + +.box .box-content .item { + padding: 0 10px; + font-size: 0.9rem; + line-height: 2.5em; +} + +.box .box-content .item .configure { + visibility: hidden; +} +.box .box-content .item:hover .configure { + visibility: visible; +} + +/*=== Tree */ +.tree { + margin: 10px 0; +} +.tree-folder-title { + position: relative; + padding: 0 10px; + line-height: 2.5rem; + font-size: 0.9rem; +} +.tree-folder-title .title { + background: inherit; + color: #fff; +} +.tree-folder-title .title:hover { + text-decoration: none; +} +.tree-folder.active .tree-folder-title { + background: linear-gradient(180deg, #222 0%, #171717 100%) #171717; + background: -webkit-linear-gradient(top, #222 0%, #171717 100%); + box-shadow: 0px 1px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset; + text-shadow: 0 0 2px rgba(255,255,255,0.28); + color: #fff; +} +.tree-folder.active > .tree-folder-title > a.title{ + color: #0090FF; + text-shadow: 0 1px rgba(255,255,255,0.08); +} +.tree-folder-items { + background: #171717; + padding: 8px 0; + box-shadow: 0 4px 4px #171717 inset, 0 1px rgba(255,255,255,0.08),0 -1px rgba(255,255,255,0.08); +} +.tree-folder-items > .item { + padding: 0 10px; + line-height: 2.5rem; + font-size: 0.8rem; +} +.tree-folder-items > .item.active { + background: linear-gradient(180deg, #222 0%, #171717 100%) #171717; + background: -webkit-linear-gradient(top, #222 0%, #171717 100%); + border-radius: 4px; + margin: 0px 8px; + box-shadow: 0px 1px #171717, 0px 1px rgba(255, 255, 255, 0.08) inset, 0 2px 2px #111; +} +.tree-folder-items > .item > a { + text-decoration: none; + color: #fff; + font-size: 0.92em; +} +.tree-folder-items > .item.active > a { + color: #0090FF +} + +/*=== STRUCTURE */ +/*===============*/ +/*=== Header */ +.header { + height: 55px; + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); + border-bottom: solid 1px #BDB7AE; + box-shadow: 0 -1px rgba(255,255,255,0.28) inset; +} +.header > .item { + padding: 0; + vertical-align: middle; + text-align: center; +} +.header > .item.title .logo { + height: 60px; + width: 60px; +} +.header > .item.title{ + width: 250px; +} +.header > .item.title h1 { + margin: 0.5em 0; +} +.header > .item.title h1 a { + text-decoration: none; + font-size: 28px; + color:#222; + text-shadow: 0 1px #fff; +} +.header > .item.search input { + width: 230px; +} +.header .item.search input:focus { + width: 350px; +} + +/*=== Body */ +#global { + background:#F9F7F4; + height: calc(100% - 60px); +} +.aside { + box-shadow: 0 2px 2px #171717 inset; + background: #222; + width: 235px; +} +.aside.aside_feed { + padding: 10px 0; + text-align: center; +} +.aside.aside_feed .tree { + position: sticky; + top: 0; + margin: 10px 0 50px; +} + +/*=== Aside main page (categories) */ +.aside_feed .tree-folder-title > .title:not([data-unread="0"]):after { + position: absolute; + right: 3px; + padding: 1px 5px; + color: #fff; + text-shadow: 0 1px rgba(255,255,255,0.08); +} +.aside_feed .btn-important { + border: none; +} + +/*=== Aside main page (feeds) */ +.feed.item.empty, +.feed.item.empty > a { + color: #e67e22; +} +.feed.item.error, +.feed.item.error > a { + color: #BD362F; +} +.aside_feed .tree-folder-items .dropdown-menu:after { + left: 2px; +} +.aside_feed .tree-folder-items .item .dropdown-target:target ~ .dropdown-toggle > .icon, +.aside_feed .tree-folder-items .item:hover .dropdown-toggle > .icon, +.aside_feed .tree-folder-items .item.active .dropdown-toggle > .icon { + border-radius: 3px; +} + +/*=== Configuration pages */ +.post { + padding: 10px 50px; + font-size: 0.9em; +} +.post form { + margin: 10px 0; +} +.post.content { + max-width: 550px; +} + +/*=== Prompt (centered) */ +.prompt { + text-align: center; + padding: 14px 0px; + text-shadow: 0 1px rgba(255,255,255,0.08); +} +.prompt label { + text-align: left; +} +.prompt form { + margin: 10px auto 20px auto; + width: 180px; +} +.prompt input { + margin: 5px auto; + width: 100%; +} +.prompt p { + margin: 20px 0; +} +.prompt input#username,.prompt input#passwordPlain{ + border:solid 1px #ccc; + box-shadow: 0 4px -4px #ccc inset,0px 1px rgba(255, 255, 255, 0.08); + background:#fff; +} +.prompt input#username:focus,.prompt input#passwordPlain:focus{ + border: solid 1px #0062BE; + box-shadow: 0 0 3px #0062BE; +} + +/*=== New article notification */ +#new-article { + background: #0084CC; + text-align: center; + font-size: 0.9em; +} +#new-article:hover { + background: #0066CC; +} +#new-article > a { + line-height: 3em; + color: #fff; + font-weight: bold; +} +#new-article > a:hover { + text-decoration: none; +} + +/*=== Day indication */ +.day { + padding: 0 10px; + font-style:italic; + line-height: 3em; + box-shadow: 0 -1px #ccc, 0 -1px rgba(255,255,255,0.28) inset; + background: #F9F7F4; + color: #666; + text-shadow: 0 1px rgba(255,255,255,0.28); + text-align: center; +} +#new-article + .day { + border-top: none; +} +.day .name { + display: none; +} + +/*=== Index menu */ +.nav_menu { + background: linear-gradient(0deg, #EDE7DE 0%, #C2BCB3 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #C2BCB3 100%); + border-bottom: 1px solid #ccc; + box-shadow:0 -1px rgba(255, 255, 255, 0.28) inset; + text-align: center; + padding: 5px 0; +} +#panel >.nav_menu{ + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); +} +#panel > .nav_menu > #nav_menu_read_all{ + background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); + border-radius: 4px; + border: 1px solid #CCC; + box-shadow: 0px 1px #FFF; +} +#panel > .nav_menu > #nav_menu_read_all > .dropdown > .btn.dropdown-toggle{ + border-radius: 0 4px 4px 0; + border:none; + border-left: solid 1px #ccc; +} + +/*=== Feed articles */ +.flux_content { + background: #FFF; +} +.flux { + background: #F9F7F4; +} +.flux:hover { + background: #F9F7F4; +} +.flux:not(.current):hover .item.title { + background: #F9F7F4; +} +.flux.current .flux .item.title a { + text-shadow:0 0 2px #ccc; +} +.flux.not_read:not(.current):hover .item.title { + opacity:0.85; +} +.flux.favorite { + background: #FFF6DA; +} +.flux.favorite:not(.current):hover{ + background: #F9F7F4; +} +.flux.favorite:not(.current):hover .item.title { + background: #F9F7F4; +} +.flux.current { + background: linear-gradient(0deg, #DAD4CB 0%, #FFF 100%) #DAD4CB; + background: -webkit-linear-gradient(bottom, #DAD4CB 0%, #FFF 100%); + box-shadow: 0 -1px #fff inset, 0 2px #ccc; + border-left: solid 4px #0062BF; +} + +.flux .item.title { +opacity: 0.35; +} +.flux.favorite .item.title { +opacity: 1; +} +.flux.not_read .item.title { +opacity: 1; +} +.flux.current .item.title a { + color: #0f0f0f; +} +.flux .item.title a { + color: #333; +} + +.flux_header { + border-top: 1px solid #ddd; + font-size: 0.8rem; + cursor: pointer; + box-shadow: 0 -1px rgba(255,255,255,0.28) inset; +} +.flux_header .title { + font-size: 0.9rem; +} +.flux .website .favicon { + padding: 5px; +} +.flux .date { + color: #666; + font-size: 0.7rem; +} + +.flux .bottom { + font-size: 0.8rem; + text-align: center; +} + +/*=== Content of feed articles */ +.content { + padding: 20px 10px; +} +.content > h1.title > a { + color: #000; +} + +.content hr { + margin: 30px 10px; + height: 1px; + background: #ddd; + border: 0; + box-shadow: 0 2px 5px #ccc; +} + +.content pre { + margin: 10px auto; + padding: 10px 20px; + overflow: auto; + background: #222; + color: #fff; + font-size: 0.9rem; + border-radius: 3px; +} +.content code { + padding: 2px 5px; + color: #dd1144; + background: #fafafa; + border: 1px solid #eee; + border-radius: 3px; +} +.content pre code { + background: transparent; + color: #fff; + border: none; +} + +.content blockquote { + display: block; + margin: 0; + padding: 5px 20px; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + background: #fafafa; + color: #333; +} +.content blockquote p { + margin: 0; +} + +/*=== Notification and actualize notification */ +.notification { + padding: 0 0 0 5px; + text-align: center; + background:#222; + border: none; + border-radius: 0 0 6px 6px; + box-shadow: 0px 0px 4px rgba(0,0,0,0.45), 0 -1px rgba(255,255,255,0.08) inset, 0 2px 2px #171717 inset; + color:#fff; + font-weight: bold; + font-size: 0.9em; + line-height: 3em; + position:absolute; + top:0; + z-index: 10; + vertical-align: middle; +} +.notification.good { + color: #fff; +} +.notification.bad { + background: #222222; + color: #EB2901; +} +.notification a.close { + padding: 0 15px; + line-height: 3em; +} +.notification#actualizeProgress { + line-height: 2em; +} + +/*=== "Load more" part */ +#bigMarkAsRead { + text-align: center; + text-decoration: none; + text-shadow: 0 -1px 0 #aaa; + color: #666; + background: #F9F7F4; + box-shadow: 0 1px rgba(255,255,255,0.28)inset; +} +#bigMarkAsRead:hover { + color: #000; + background: #F9F7F4; + background: radial-gradient(circle at 50% -25% , #ccc 0%, #F9F7F4 50%); +} +#bigMarkAsRead:hover .bigTick { + text-shadow: 0 0 10px #666; +} + +/*=== Navigation menu (for articles) */ +#nav_entries { + background: linear-gradient(180deg, #222 0%, #171717 100%) #222; + background: -webkit-linear-gradient(top, #222 0%, #171717 100%); + border-top: 1px solid #171717; + text-align: center; + line-height: 3em; + table-layout: fixed; + box-shadow: 0 1px rgba(255,255,255,0.08) inset, 0 -2px 2px #171717; + width:235px; +} + +/*=== READER VIEW */ +/*================*/ +#stream.reader .flux { + padding: 0 0 50px; + border: none; + background: #f0f0f0; + color: #333; +} +#stream.reader .flux .author { + margin: 0 0 10px; + font-size: 90%; + color: #666; +} + +/*=== GLOBAL VIEW */ +/*================*/ +#stream.global { + padding: 24px 0; +} + +.box.category .box-title { + background: linear-gradient(0deg, #EDE7DE 0%, #fff 100%) #171717; + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #fff 100%); + box-shadow: 0px -1px #fff inset,0 -2px #ccc inset; + border-radius: none; + line-height: 2em; + font-size: 1.2rem; + text-shadow:0 1px #ccc; +} +.box.category .box-title .title { + font-weight: normal; + text-decoration: none; + text-align: left; + color: #888; +} +.box.category:not([data-unread="0"]) .box-title { +} +.box.category:not([data-unread="0"]) .box-title:active { +} +.box.category:not([data-unread="0"]) .box-title .title { + color: #222; + font-weight: bold; +} +.box.category .title:not([data-unread="0"]):after { + position: absolute; + top: 5px; right: 10px; + border: 0; + background: none; + font-weight: bold; +} +.box.category .item.feed { + padding: 2px 10px; + font-size: 0.8rem; +} +.box.category .item.feed:not(.empty):not(.error) .item-title { + color: #222; +} + +/*=== PANEL */ +/*===========*/ +#panel { + box-shadow: 0px 0px 4px #000; + border-radius: 8px; + background:#F9F7F4; +} +/*=== DIVERS */ +/*===========*/ +.aside.aside_feed .nav-form input,.aside.aside_feed .nav-form select { + width: 130px; +} +.aside.aside_feed .nav-form .dropdown .dropdown-menu { + right: -20px; +} +.aside.aside_feed .nav-form .dropdown .dropdown-menu:after { + right: 33px; +} + +/*=== STATISTICS */ +/*===============*/ +.stat { + margin: 10px 0 20px; +} + +.stat th, +.stat td, +.stat tr { + border: none; +} +.stat > table td, +.stat > table th { + border-bottom: 1px solid #ccc; + background: rgba(255,255,255,0.38); + box-shadow: 0 1px #fff; +} + +.stat > .horizontal-list { + margin: 0 0 5px; +} +.stat > .horizontal-list .item { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.stat > .horizontal-list .item:first-child { + width: 250px; +} + +/*=== LOGS */ +/*=========*/ +.logs { + border: 1px solid #aaa; + border-radius: 5px; + overflow: hidden; +} +.log { + padding: 5px 10px; + background: #fafafa; + color: #333; + font-size: 0.8rem; +} +.log+.log { + border-top: 1px solid #aaa; +} +.log .date { + display: block; + font-weight: bold; +} +.log.error { + background: #fdd; + color: #844; +} +.log.warning { + background: #ffe; + color: #c95; +} +.log.notice { + background: #f4f4f4; + color: #aaa; +} +.log.debug { + background: #333; + color: #eee; +} + +#slider.active { + box-shadow: -4px 0 4px rgba(15, 15, 15, 0.55); + background: #F8F8F8; +} +#close-slider.active { + background: rgba(15, 15, 15, 0.35); +} + +/*=== MOBILE */ +/*===========*/ +@media screen and (max-width: 840px) { + .header { + display: table; + } + .nav-login { + display: none; + } + + .aside { + width: 0; + border-top: none; + box-shadow: 3px 0 3px #000; + transition: width 200ms linear; + -moz-transition: width 200ms linear; + -webkit-transition: width 200ms linear; + -o-transition: width 200ms linear; + -ms-transition: width 200ms linear; + } + .aside:target { + width: 235px; + } + .aside .toggle_aside, + #panel .close { + display: block; + width: 100%; + height: 40px; + line-height: 40px; + text-align: center; + background: #171717; + box-shadow: 0 1px rgba(255,255,255,0.08); + } + .aside .btn-important { + display: inline-block; + margin: 20px 0 0; + } + + .aside.aside_feed { + padding: 0; + } + .aside.aside_feed .tree { + position: static; + } + + .nav_menu .btn { + margin: 5px 10px; + } + .nav_menu .stick { + margin: 0 10px; + } + .nav_menu .stick .btn { + margin: 5px 0; + } + .nav_menu .search { + display: inline-block; + max-width: 97%; + } + .nav_menu .search input { + max-width: 97%; + width: 90px; + } + .nav_menu .search input:focus { + width: 400px; + } + + .day .name { + display: none; + } + + .pagination { + margin: 0 0 3.5em; + } + + .notification a.close { + display: block; + left: 0; + background: transparent; + } + .notification a.close:hover { + opacity: 0.5; + } + .notification a.close .icon { + display: none; + } + .nav_menu .search { + display: none; + } + + #nav_entries { + width: 100%; + } +} + +@media (max-width: 700px) { + .header{ + display: none; + } + .nav-login { + display: inline-block; + width: 100%; + } + .nav_menu .search { + display: inline-block; + } + .aside .btn-important { + display: none; + } +} diff --git a/p/themes/BlueLagoon/README.md b/p/themes/BlueLagoon/README.md new file mode 100644 index 000000000..62afc234b --- /dev/null +++ b/p/themes/BlueLagoon/README.md @@ -0,0 +1,38 @@ +Blue Lagoon +======= + +**C'est un cocktail (bis)! C'est la version plus "fresh" de [Screwdriver](https://github.com/misterair/Screwdriver). C'est... c'est... un thème pour l'agrégateur de flux RSS [FreshRSS](https://github.com/marienfressinaud/FreshRSS/)** + + +En toute modestie, ce thème tue du Nyan Cat. + +![screenshot](https://raw.githubusercontent.com/misterair/BlueLagoon/master/screenshot.png) + + +Installation +----------------- +1. Placez le dossier du thème dans ledossier /FreshRSS/p/themes/Screwdriver de votre FreshRSS; +2. Allez dans les paramètres d'Affichage et changez de thème; +3. Profitez de votre Blue Laggon sans modération! +4. Remontez les problèmes sur Github (*facultatif mais fortement apprécié*) + + + +Blue Lagoon est distribué sous license AlcoholWare: +----------------- + +« LICENCE ALCOHOLWARE » (Révision 42): + +mister.air@gmail.com a créé ce fichier. Tant que vous conservez cet avertissement, + +vous pouvez faire ce que vous voulez de ce truc. Si on se rencontre un jour et + +que vous pensez que ce truc vaut le coup, vous pouvez me payer un verre (rempli) en retour. + +*Mister aiR* + + + + + + diff --git a/p/themes/BlueLagoon/icons/bookmark.svg b/p/themes/BlueLagoon/icons/bookmark.svg new file mode 100644 index 000000000..b77dc5518 --- /dev/null +++ b/p/themes/BlueLagoon/icons/bookmark.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/favicon-16-32-48-64.ico b/p/themes/BlueLagoon/icons/favicon-16-32-48-64.ico new file mode 100644 index 000000000..7c5eb5517 Binary files /dev/null and b/p/themes/BlueLagoon/icons/favicon-16-32-48-64.ico differ diff --git a/p/themes/BlueLagoon/icons/favicon-256.png b/p/themes/BlueLagoon/icons/favicon-256.png new file mode 100644 index 000000000..dbe4ec4b7 Binary files /dev/null and b/p/themes/BlueLagoon/icons/favicon-256.png differ diff --git a/p/themes/BlueLagoon/icons/favicon.svg b/p/themes/BlueLagoon/icons/favicon.svg new file mode 100644 index 000000000..ac816d68c --- /dev/null +++ b/p/themes/BlueLagoon/icons/favicon.svg @@ -0,0 +1,273 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/icon.svg b/p/themes/BlueLagoon/icons/icon.svg new file mode 100644 index 000000000..083eeb685 --- /dev/null +++ b/p/themes/BlueLagoon/icons/icon.svg @@ -0,0 +1,291 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/non-starred.svg b/p/themes/BlueLagoon/icons/non-starred.svg new file mode 100644 index 000000000..2762b060a --- /dev/null +++ b/p/themes/BlueLagoon/icons/non-starred.svg @@ -0,0 +1,59 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/read.svg b/p/themes/BlueLagoon/icons/read.svg new file mode 100644 index 000000000..28980576b --- /dev/null +++ b/p/themes/BlueLagoon/icons/read.svg @@ -0,0 +1,72 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/starred.svg b/p/themes/BlueLagoon/icons/starred.svg new file mode 100644 index 000000000..4c749ce26 --- /dev/null +++ b/p/themes/BlueLagoon/icons/starred.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/unread.svg b/p/themes/BlueLagoon/icons/unread.svg new file mode 100644 index 000000000..596dec0ac --- /dev/null +++ b/p/themes/BlueLagoon/icons/unread.svg @@ -0,0 +1,65 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/p/themes/BlueLagoon/loader.gif b/p/themes/BlueLagoon/loader.gif new file mode 100644 index 000000000..a0839dcc7 Binary files /dev/null and b/p/themes/BlueLagoon/loader.gif differ diff --git a/p/themes/BlueLagoon/metadata.json b/p/themes/BlueLagoon/metadata.json new file mode 100644 index 000000000..2c415a613 --- /dev/null +++ b/p/themes/BlueLagoon/metadata.json @@ -0,0 +1,7 @@ +{ + "name": "Blue Lagoon", + "author": "Mister aiR", + "description": "C'est un cocktail (bis)! C'est la version plus fresh de Screwdriver. C'est... c'est... un thème pour l'agrégateur de flux RSS FreshRSS. En toute modestie, ce thème tue du Nyan Cat.", + "version": 1.0, + "files": ["_template.css","BlueLagoon.css"] +} diff --git a/p/themes/BlueLagoon/template.css b/p/themes/BlueLagoon/template.css new file mode 100644 index 000000000..bf421e322 --- /dev/null +++ b/p/themes/BlueLagoon/template.css @@ -0,0 +1,695 @@ +@charset "UTF-8"; + +/*=== GENERAL */ +/*============*/ +html, body { + margin: 0; + padding: 0; + font-size: 92%; +} + +/*=== Links */ +a { + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +/*=== Lists */ +ul, ol, dd { + margin: 0; + padding: 0; +} + +/*=== Titles */ +h1 { + margin: 0.6em 0 0.3em; + font-size: 1.5em; + line-height: 1.6em; +} +h2 { + margin: 0.5em 0 0.25em; + font-size: 1.3em; + line-height: 2em; +} +h3 { + margin: 0.5em 0 0.25em; + font-size: 1.1em; + line-height: 2em; +} + +/*=== Paragraphs */ +p { + margin: 1em 0 0.5em; + font-size: 1em; +} + +/*=== Images */ +img { + height: auto; + max-width: 100%; +} +img.favicon { + height: 16px; + width: 16px; + vertical-align: middle; +} + +/*=== Videos */ +iframe, embed, object, video { + max-width: 100%; +} + +/*=== Forms */ +legend { + display: block; + width: 100%; + clear: both; +} +label { + display: block; +} +input { + width: 180px; +} +textarea { + width: 300px; +} +input, select, textarea { + display: inline-block; + max-width: 100%; +} +input[type="radio"], +input[type="checkbox"] { + width: 15px !important; + min-height: 15px !important; +} +input.extend:focus { + width: 300px; +} + +/*=== COMPONENTS */ +/*===============*/ +/*=== Forms */ +.form-group:after { + content: ""; + display: block; + clear: both; +} +.form-group.form-actions { + min-width: 250px; +} +.form-group .group-name { + display: block; + float: left; + width: 200px; +} +.form-group .group-controls { + min-width: 250px; + margin: 0 0 0 220px; +} +.form-group .group-controls .control { + display: block; +} + +/*=== Buttons */ +.stick { + display: inline-block; + white-space: nowrap; +} +.btn, +a.btn { + display: inline-block; + cursor: pointer; + overflow: hidden; +} +.btn-important { + font-weight: bold; +} + +/*=== Navigation */ +.nav-list .nav-header, +.nav-list .item { + display: block; +} +.nav-list .item, +.nav-list .item > a { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.nav-head { + display: block; +} +.nav-head .item { + display: inline-block; +} + +/*=== Horizontal-list */ +.horizontal-list { + display: table; + table-layout: fixed; + width: 100%; +} +.horizontal-list .item { + display: table-cell; +} + +/*=== Dropdown */ +.dropdown { + position: relative; + display: inline-block; +} +.dropdown-target { + display: none; +} +.dropdown-menu { + display: none; + min-width: 200px; + margin: 0; + position: absolute; + right: 0; + background: #fff; + border: 1px solid #aaa; +} +.dropdown-header { + display: block; +} +.dropdown-menu > .item { + display: block; +} +.dropdown-menu > .item > a, +.dropdown-menu > .item > span { + display: block; +} +.dropdown-menu > .item[aria-checked="true"] > a:before { + content: '✓'; +} +.dropdown-menu .input { + display: block; +} +.dropdown-menu .input select, +.dropdown-menu .input input { + display: block; + max-width: 95%; +} +.dropdown-target:target ~ .dropdown-menu { + display: block; + z-index: 10; +} +.dropdown-close { + display: inline; +} +.dropdown-close a { + font-size: 0; + position: fixed; + top: 0; bottom: 0; + left: 0; right: 0; + display: block; + z-index: -10; +} +.separator { + display: block; + height: 0; + border-bottom: 1px solid #aaa; +} + +/*=== Alerts */ +.alert { + display: block; + width: 90%; +} +.group-controls .alert { + width: 100% +} +.alert-head { + margin: 0; + font-weight: bold; +} +.alert ul { + margin: 5px 20px; +} + +/*=== Icons */ +.icon { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; + line-height: 16px; +} + +/*=== Pagination */ +.pagination { + display: table; + width: 100%; + margin: 0; + padding: 0; + table-layout: fixed; +} +.pagination .item { + display: table-cell; +} +.pagination .pager-first, +.pagination .pager-previous, +.pagination .pager-next, +.pagination .pager-last { + width: 100px; +} + +/*=== STRUCTURE */ +/*===============*/ +/*=== Header */ +.header { + display: table; + width: 100%; + table-layout: fixed; +} +.header > .item { + display: table-cell; +} +.header > .item.title { + width: 250px; + white-space: nowrap; +} +.header > .item.title h1 { + display: inline-block; +} +.header > .item.title .logo { + display: inline-block; + height: 32px; + width: 32px; + vertical-align: middle; +} +.header > .item.configure { + width: 100px; +} + +/*=== Body */ +#global { + display: table; + width: 100%; + height: 100%; + table-layout: fixed; +} +.aside { + display: table-cell; + height: 100%; + width: 250px; + vertical-align: top; +} +.aside.aside_flux { + background: #fff; +} + +/*=== Aside main page (categories) */ +.categories { + list-style: none; + margin: 0; +} +.category { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.category .btn:not([data-unread="0"]):after { + content: attr(data-unread); +} + +/*=== Aside main page (feeds) */ +.categories .feeds { + width: 100%; + list-style: none; +} +.categories .feeds:not(.active) { + display: none; +} +.categories .feeds .feed { + display: inline-block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} +.categories .feeds .feed:not([data-unread="0"]):before { + content: "(" attr(data-unread) ") "; +} +.categories .feeds .dropdown-menu { + left: 0; +} +.categories .feeds .item .dropdown-toggle > .icon { + visibility: hidden; + cursor: pointer; + vertical-align: top; +} +.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon, +.categories .feeds .item:hover .dropdown-toggle > .icon, +.categories .feeds .item.active .dropdown-toggle > .icon { + visibility: visible; +} + +/*=== New article notification */ +#new-article { + display: none; +} +#new-article > a { + display: block; +} + +/*=== Day indication */ +.day .name { + position: absolute; + right: 0; + width: 50%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/*=== Feed article header and footer */ +.flux_header { + position: relative; +} +.flux .item { + line-height: 40px; + white-space: nowrap; +} +.flux .item.manage, +.flux .item.link { + width: 40px; + text-align: center; +} +.flux .item.website { + width: 200px; +} +.flux.not_read .item.title, +.flux.current .item.title { + font-weight: bold; +} +.flux:not(.current):hover .item.title { + position: absolute; + max-width: calc(100% - 320px); + background: #fff; +} +.flux .item.title a { + color: #000; + text-decoration: none; +} +.flux .item.date { + width: 145px; + text-align: right; +} +.flux .item > a { + display: block; +} +.flux .item > a { + display: block; + text-decoration: none; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.flux .item.share > a { + display: list-item; + list-style-position: inside; + list-style-type: decimal; +} + +/*=== Feed article content */ +.hide_posts > .flux:not(.active) > .flux_content { + display: none; +} +.content { + min-height: 20em; + margin: auto; + line-height: 1.7em; + word-wrap: break-word; +} +.content.large { + max-width: 1000px; +} +.content.medium { + max-width: 800px; +} +.content.thin { + max-width: 550px; +} +.content ul, +.content ol, +.content dd { + margin: 0 0 0 15px; + padding: 0 0 5px 15px; +} +.content pre { + overflow: auto; +} + +/*=== Notification and actualize notification */ +.notification { + position: absolute; + top: 1em; + left: 25%; right: 25%; + z-index: 10; + background: #fff; + border: 1px solid #aaa; +} +.notification.closed { + display: none; +} +.notification a.close { + position: absolute; + top: 0; bottom: 0; + right: 0; + display: inline-block; +} + +#actualizeProgress { + position: fixed; +} +#actualizeProgress progress { + max-width: 100%; + vertical-align: middle; +} +#actualizeProgress .progress { + vertical-align: middle; +} + +/*=== Navigation menu (for articles) */ +#nav_entries { + position: fixed; + bottom: 0; left: 0; + display: table; + width: 250px; + background: #fff; + table-layout: fixed; +} +#nav_entries .item { + display: table-cell; + width: 30%; +} +#nav_entries a { + display: block; +} + +/*=== "Load more" part */ +#load_more { + min-height: 40px; +} +.loading { + background: url("loader.gif") center center no-repeat; + font-size: 0; +} +#bigMarkAsRead { + display: block; + padding: 3em 0; + text-align: center; +} +.bigTick { + font-size: 7em; + line-height: 1.6em; +} + +/*=== Statistiques */ +.stat > table { + width: 100%; +} + +/*=== GLOBAL VIEW */ +/*================*/ +/*=== Category boxes */ +#stream.global .box-category { + display: inline-block; + width: 19em; + max-width: 95%; + margin: 20px 10px; + border: 1px solid #ccc; + vertical-align: top; +} +#stream.global .category { + width: 100%; +} +#stream.global .btn { + display: block; +} +#stream.global .box-category .feeds { + display: block; + overflow: auto; +} +#stream.global .box-category .feed { + width: 19em; + max-width: 90%; +} + +/*=== Panel */ +#overlay { + display: none; + position: fixed; + top: 0; bottom: 0; + left: 0; right: 0; + background: rgba(0, 0, 0, 0.9); +} +#panel { + display: none; + position: fixed; + top: 1em; bottom: 1em; + left: 2em; right: 2em; + overflow: auto; + background: #fff; +} +#panel .close { + position: fixed; + top: 0; bottom: 0; + left: 0; right: 0; + display: block; +} +#panel .close img { + display: none; +} + +/*=== DIVERS */ +/*===========*/ +.nav-login, +.nav_menu .search, +.nav_menu .toggle_aside { + display: none; +} + +.aside .toggle_aside { + position: absolute; + right: 0; + display: none; + width: 30px; + height: 30px; + line-height: 30px; + text-align: center; +} + +/*=== MOBILE */ +/*===========*/ +@media(max-width: 840px) { + .header, + .aside .btn-important, + .aside .feeds .dropdown, + .flux_header .item.website span, + .item.date, .day .date, + .dropdown-menu > .no-mobile, + .no-mobile { + display: none; + } + .nav-login { + display: block; + } + .nav_menu .toggle_aside, + .aside .toggle_aside, + .nav_menu .search, + #panel .close img { + display: inline-block; + } + + .aside { + position: fixed; + top: 0; bottom: 0; + left: 0; + width: 0; + overflow: hidden; + z-index: 100; + } + .aside:target { + width: 90%; + overflow: auto; + } + .aside .categories { + margin: 10px 0 75px; + } + + .flux_header .item.website { + width: 40px; + } + + .flux:not(.current):hover .item.title { + position: relative; + width: auto; + white-space: nowrap; + } + + .notification { + top: 0; + left: 0; + right: 0; + } + + #nav_entries { + width: 100%; + } + + #stream.global .box-category { + margin: 10px 0; + } + + #panel { + top: 0; bottom: 0; + left: 0; right: 0; + } + #panel .close { + top: 0; right: 0; + left: auto; bottom: auto; + display: inline-block; + width: 30px; + height: 30px; + } +} + +/*=== PRINTER */ +/*============*/ +@media print { + .header, .aside, + .nav_menu, .day, + .flux_header, + .flux_content .bottom, + .pagination, + #nav_entries { + display: none; + } + html, body { + background: #fff; + color: #000; + font-family: Serif; + } + #global, + .flux_content { + display: block !important; + } + .flux_content .content { + width: 100% !important; + } + .flux_content .content a { + color: #000; + } + .flux_content .content a:after { + content: " [" attr(href) "] "; + font-style: italic; + } +} diff --git a/p/themes/BlueLagoon/thumbs/original.png b/p/themes/BlueLagoon/thumbs/original.png new file mode 100644 index 000000000..bb8695945 Binary files /dev/null and b/p/themes/BlueLagoon/thumbs/original.png differ -- cgit v1.2.3 From 498f8315f724a5e85b28907c3bc4dc08699d679e Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 26 Nov 2014 17:54:41 -0500 Subject: Optimize bluelagoon theme icons. In the future, we should pay attention to those icons since I've done that a couple of time now. We should have some documentation for new contributions. --- p/themes/BlueLagoon/icons/bookmark.svg | 64 +----- p/themes/BlueLagoon/icons/favicon.svg | 303 +++------------------------- p/themes/BlueLagoon/icons/icon.svg | 321 +++--------------------------- p/themes/BlueLagoon/icons/non-starred.svg | 62 +----- p/themes/BlueLagoon/icons/read.svg | 74 +------ p/themes/BlueLagoon/icons/starred.svg | 63 +----- p/themes/BlueLagoon/icons/unread.svg | 66 +----- 7 files changed, 80 insertions(+), 873 deletions(-) diff --git a/p/themes/BlueLagoon/icons/bookmark.svg b/p/themes/BlueLagoon/icons/bookmark.svg index b77dc5518..7f33d9753 100644 --- a/p/themes/BlueLagoon/icons/bookmark.svg +++ b/p/themes/BlueLagoon/icons/bookmark.svg @@ -1,61 +1,5 @@ - - - - - - image/svg+xml - - - - - - - - - + + + + diff --git a/p/themes/BlueLagoon/icons/favicon.svg b/p/themes/BlueLagoon/icons/favicon.svg index ac816d68c..1b6801a26 100644 --- a/p/themes/BlueLagoon/icons/favicon.svg +++ b/p/themes/BlueLagoon/icons/favicon.svg @@ -1,273 +1,32 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/icon.svg b/p/themes/BlueLagoon/icons/icon.svg index 083eeb685..8abfea179 100644 --- a/p/themes/BlueLagoon/icons/icon.svg +++ b/p/themes/BlueLagoon/icons/icon.svg @@ -1,291 +1,34 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/p/themes/BlueLagoon/icons/non-starred.svg b/p/themes/BlueLagoon/icons/non-starred.svg index 2762b060a..d5c1f5ee0 100644 --- a/p/themes/BlueLagoon/icons/non-starred.svg +++ b/p/themes/BlueLagoon/icons/non-starred.svg @@ -1,59 +1,5 @@ - - - - - - image/svg+xml - - - - - - - - - + + + + diff --git a/p/themes/BlueLagoon/icons/read.svg b/p/themes/BlueLagoon/icons/read.svg index 28980576b..269b75738 100644 --- a/p/themes/BlueLagoon/icons/read.svg +++ b/p/themes/BlueLagoon/icons/read.svg @@ -1,72 +1,4 @@ - - - - - - image/svg+xml - - - - - - - - - - - + + + diff --git a/p/themes/BlueLagoon/icons/starred.svg b/p/themes/BlueLagoon/icons/starred.svg index 4c749ce26..10a16f04a 100644 --- a/p/themes/BlueLagoon/icons/starred.svg +++ b/p/themes/BlueLagoon/icons/starred.svg @@ -1,60 +1,5 @@ - - - - - - image/svg+xml - - - - - - - - - + + + + diff --git a/p/themes/BlueLagoon/icons/unread.svg b/p/themes/BlueLagoon/icons/unread.svg index 596dec0ac..bd39d683a 100644 --- a/p/themes/BlueLagoon/icons/unread.svg +++ b/p/themes/BlueLagoon/icons/unread.svg @@ -1,65 +1,3 @@ - - - - - - image/svg+xml - - - - - - - - - - + + -- cgit v1.2.3 From 82745a509a1284a9fe6a7f8d5d153eda953d9e11 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 26 Nov 2014 18:14:17 -0500 Subject: Optimize screwdriver theme icons. --- p/themes/Screwdriver/icons/favicon.svg | 312 ++++----------------------------- p/themes/Screwdriver/icons/icon.svg | 303 ++++---------------------------- 2 files changed, 66 insertions(+), 549 deletions(-) diff --git a/p/themes/Screwdriver/icons/favicon.svg b/p/themes/Screwdriver/icons/favicon.svg index aaab5de99..895a727bb 100644 --- a/p/themes/Screwdriver/icons/favicon.svg +++ b/p/themes/Screwdriver/icons/favicon.svg @@ -1,280 +1,34 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/p/themes/Screwdriver/icons/icon.svg b/p/themes/Screwdriver/icons/icon.svg index 7f3d76af2..268814463 100644 --- a/p/themes/Screwdriver/icons/icon.svg +++ b/p/themes/Screwdriver/icons/icon.svg @@ -1,271 +1,34 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3 From adaec6176ca04ee38306bd348eba9a8350f19405 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Tue, 2 Dec 2014 23:34:31 -0500 Subject: Add article auto-remove after clicking on the read link --- p/scripts/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/p/scripts/main.js b/p/scripts/main.js index d1d31c801..19eba206d 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -690,6 +690,9 @@ function init_stream(divStream) { divStream.on('click', '.flux a.read', function () { var active = $(this).parents(".flux"); mark_read(active, false); + if (context['auto_remove_article']) { + active.remove(); + } return false; }); -- cgit v1.2.3 From aa232d8fd52cf548c0b2e7d96e026e684eadda53 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 3 Dec 2014 16:45:51 +0100 Subject: Update the i18n system. Follow recommendations from https://github.com/FreshRSS/FreshRSS/issues/334 --- app/i18n/en.php | 533 ----------------------------------------------- app/i18n/en/admin.php | 92 ++++++++ app/i18n/en/conf.php | 7 + app/i18n/en/feedback.php | 14 ++ app/i18n/en/gen.php | 487 +++++++++++++++++++++++++++++++++++++++++++ app/i18n/en/index.php | 5 + app/i18n/en/install.php | 5 + app/i18n/en/sub.php | 10 + app/i18n/fr.php | 533 ----------------------------------------------- app/i18n/fr/admin.php | 92 ++++++++ app/i18n/fr/conf.php | 7 + app/i18n/fr/feedback.php | 14 ++ app/i18n/fr/gen.php | 487 +++++++++++++++++++++++++++++++++++++++++++ app/i18n/fr/index.php | 5 + app/i18n/fr/install.php | 5 + app/i18n/fr/sub.php | 10 + lib/Minz/Translate.php | 114 +++++++--- 17 files changed, 1320 insertions(+), 1100 deletions(-) delete mode 100644 app/i18n/en.php create mode 100644 app/i18n/en/admin.php create mode 100644 app/i18n/en/conf.php create mode 100644 app/i18n/en/feedback.php create mode 100644 app/i18n/en/gen.php create mode 100644 app/i18n/en/index.php create mode 100644 app/i18n/en/install.php create mode 100644 app/i18n/en/sub.php delete mode 100644 app/i18n/fr.php create mode 100644 app/i18n/fr/admin.php create mode 100644 app/i18n/fr/conf.php create mode 100644 app/i18n/fr/feedback.php create mode 100644 app/i18n/fr/gen.php create mode 100644 app/i18n/fr/index.php create mode 100644 app/i18n/fr/install.php create mode 100644 app/i18n/fr/sub.php diff --git a/app/i18n/en.php b/app/i18n/en.php deleted file mode 100644 index a35a6ccf1..000000000 --- a/app/i18n/en.php +++ /dev/null @@ -1,533 +0,0 @@ - '\\A\\p\\r\\i\\l', - 'Aug' => '\\A\\u\\g\\u\\s\\t', - 'Dec' => '\\D\\e\\c\\e\\m\\b\\e\\r', - 'Feb' => '\\F\\e\\b\\r\\u\\a\\r\\y', - 'Jan' => '\\J\\a\\n\\u\\a\\r\\y', - 'Jul' => '\\J\\u\\l\\y', - 'Jun' => '\\J\\u\\n\\e', - 'Mar' => '\\M\\a\\r\\c\\h', - 'May' => '\\M\\a\\y', - 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r', - 'Oct' => '\\O\\c\\t\\o\\b\\e\\r', - 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r', - 'about' => 'About', - 'about_freshrss' => 'About FreshRSS', - 'access_denied' => 'You don’t have permission to access this page', - 'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds', - 'activate_sharing' => 'Activate sharing', - 'actualize' => 'Actualize', - 'add_category' => 'Add a category', - 'add_query' => 'Add a query', - 'add_rss_feed' => 'Add a RSS feed', - 'admin.check_install.cache.nok' => 'Check permissions on ./data/cache directory. HTTP server must have rights to write into', - 'admin.check_install.cache.ok' => 'Permissions on cache directory are good.', - 'admin.check_install.categories.nok' => 'Category table is bad configured.', - 'admin.check_install.categories.ok' => 'Category table is ok.', - 'admin.check_install.connection.nok' => 'Connection to the database cannot being established.', - 'admin.check_install.connection.ok' => 'Connection to the database is ok.', - 'admin.check_install.ctype.nok' => 'You lack a required library for character type checking (php-ctype).', - 'admin.check_install.ctype.ok' => 'You have the required library for character type checking (ctype).', - 'admin.check_install.curl.nok' => 'You lack cURL (php5-curl package).', - 'admin.check_install.curl.ok' => 'You have cURL extension.', - 'admin.check_install.data.nok' => 'Check permissions on ./data directory. HTTP server must have rights to write into', - 'admin.check_install.data.ok' => 'Permissions on data directory are good.', - 'admin.check_install.database' => 'Database installation', - 'admin.check_install.dom.nok' => 'You lack a required library to browse the DOM (php-xml package).', - 'admin.check_install.dom.ok' => 'You have the required library to browse the DOM.', - 'admin.check_install.entries.nok' => 'Entry table is bad configured.', - 'admin.check_install.entries.ok' => 'Entry table is ok.', - 'admin.check_install.favicons.nok' => 'Check permissions on ./data/favicons directory. HTTP server must have rights to write into', - 'admin.check_install.favicons.ok' => 'Permissions on favicons directory are good.', - 'admin.check_install.feeds.nok' => 'Feed table is bad configured.', - 'admin.check_install.feeds.ok' => 'Feed table is ok.', - 'admin.check_install.files' => 'File installation', - 'admin.check_install.json.nok' => 'You lack JSON (php5-json package).', - 'admin.check_install.json.ok' => 'You have JSON extension.', - 'admin.check_install.logs.nok' => 'Check permissions on ./data/logs directory. HTTP server must have rights to write into', - 'admin.check_install.logs.ok' => 'Permissions on logs directory are good.', - 'admin.check_install.minz.nok' => 'You lack the Minz framework.', - 'admin.check_install.minz.ok' => 'You have the Minz framework.', - 'admin.check_install.pcre.nok' => 'You lack a required library for regular expressions (php-pcre).', - 'admin.check_install.pcre.ok' => 'You have the required library for regular expressions (PCRE).', - 'admin.check_install.pdo.nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).', - 'admin.check_install.pdo.ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).', - 'admin.check_install.persona.nok' => 'Check permissions on ./data/persona directory. HTTP server must have rights to write into', - 'admin.check_install.persona.ok' => 'Permissions on Mozilla Persona directory are good.', - 'admin.check_install.php' => 'PHP installation', - 'admin.check_install.php.nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.', - 'admin.check_install.php.ok' => 'Your PHP version is %s, which is compatible with FreshRSS.', - 'admin.check_install.tables.nok' => 'There is one or more lacking tables in the database.', - 'admin.check_install.tables.ok' => 'Tables are existing in the database.', - 'admin.check_install.tokens.nok' => 'Check permissions on ./data/tokens directory. HTTP server must have rights to write into', - 'admin.check_install.tokens.ok' => 'Permissions on tokens directory are good.', - 'admin.check_install.zip.nok' => 'You lack ZIP extension (php5-zip package).', - 'admin.check_install.zip.ok' => 'You have ZIP extension.', - 'admin.users.articles_and_size' => '%s articles (%s)', - 'administration' => 'Manage', - 'advanced' => 'Advanced', - 'after_onread' => 'After “mark all as read”,', - 'agpl3' => 'AGPL 3', - 'all_feeds' => 'All feeds', - 'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)', - 'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles', - 'already_subscribed' => 'You have already subscribed to %s', - 'api_enabled' => 'Allow API access (required for mobile apps)', - 'apr' => 'apr', - 'april' => 'Apr', - 'archiving_configuration' => 'Archiving', - 'archiving_configuration_help' => 'More options are available in the individual stream settings', - 'article' => 'Article', - 'article_icons' => 'Article icons', - 'article_open_on_website' => 'when article is opened on its original website', - 'article_published_on' => 'This article originally appeared on %s', - 'article_published_on_author' => 'This article originally appeared on %s by %s', - 'article_viewed' => 'when article is viewed', - 'articles' => 'articles', - 'articles_per_page' => 'Number of articles per page', - 'articles_to_display' => 'Articles to display', - 'ask_empty' => 'Clear?', - 'attention' => 'Attention!', - 'aug' => 'aug', - 'august' => 'Aug', - 'auth_form' => 'Web form (traditional, requires JavaScript)', - 'auth_form_not_set' => 'A problem occured during authentication system configuration. Please retry later.', - 'auth_form_set' => 'Form is now your default authentication system.', - 'auth_no_password_set' => 'Administrator password hasn’t been set. This feature isn’t available.', - 'auth_none' => 'None (dangerous)', - 'auth_not_persona' => 'Only Persona system can be reset.', - 'auth_persona' => 'Mozilla Persona (modern, requires JavaScript)', - 'auth_reset' => 'Authentication reset', - 'auth_token' => 'Authentication token', - 'auth_type' => 'Authentication method', - 'auth_will_reset' => 'Authentication system will be reset: a form will be used instead of Persona.', - 'author' => 'Author', - 'auto_load_more' => 'Load next articles at the page bottom', - 'auto_read_when' => 'Mark article as read…', - 'auto_share' => 'Share', - 'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.', - 'back_to_rss_feeds' => '← Go back to your RSS feeds', - 'bad_opml_file' => 'Your OPML file is invalid', - 'base_url' => 'Base URL', - 'bdd' => 'Database', - 'bdd_conf_is_ko' => 'Verify your database information.', - 'bdd_conf_is_ok' => 'Database configuration has been saved.', - 'bdd_configuration' => 'Database configuration', - 'bdd_type' => 'Type of database', - 'before_one_day' => 'Before one day', - 'before_one_week' => 'Before one week', - 'before_yesterday' => 'Before yesterday', - 'blank_to_disable' => 'Leave blank to disable', - 'blogotext' => 'Blogotext', - 'bookmark' => 'Subscribe (FreshRSS bookmark)', - 'bottom_line' => 'Bottom line', - 'bugs_reports' => 'Bugs reports', - 'by' => 'by', - 'by_author' => 'By %s', - 'by_default' => 'By default', - 'by_email' => 'By email', - 'by_feed' => 'by feed', - 'cache_is_ok' => 'Permissions on cache directory are good', - 'can_not_be_deleted' => 'Cannot be deleted', - 'cancel' => 'Cancel', - 'categories' => 'Categories', - 'categories_management' => 'Categories management', - 'categories_updated' => 'Categories have been updated', - 'categorize' => 'Store in a category', - 'category' => 'Category', - 'category_created' => 'Category %s has been created.', - 'category_deleted' => 'Category has been deleted.', - 'category_emptied' => 'Category has been emptied', - 'category_empty' => 'Empty category', - 'category_name_exists' => 'Category name already exists.', - 'category_no_id' => 'You must precise the id of the category.', - 'category_no_name' => 'Category name cannot be empty.', - 'category_not_delete_default' => 'You cannot delete the default category!', - 'category_not_exist' => 'The category does not exist!', - 'category_number' => 'Category n°%d', - 'category_updated' => 'Category has been updated.', - 'change_value' => 'You should change this value by any other', - 'checks' => 'Checks', - 'choose_language' => 'Choose a language for FreshRSS', - 'clear_logs' => 'Clear the logs', - 'collapse_article' => 'Collapse', - 'conf.users.articles_and_size' => '%s articles (%s)', - 'configuration' => 'Configuration', - 'configuration_updated' => 'Configuration has been updated', - 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', - 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!', - 'congratulations' => 'Congratulations!', - 'content_width' => 'Content width', - 'create' => 'Create', - 'create_user' => 'Create new user', - 'credits' => 'Credits', - 'credits_content' => 'Some design elements come from Bootstrap although FreshRSS doesn’t use this framework. Icons come from GNOME project. Open Sans font police has been created by Steve Matteson. Favicons are collected with getFavicon API. FreshRSS is based on Minz, a PHP framework.', - 'css_path_on_website' => 'Articles CSS path on original website', - 'ctype_is_nok' => 'You lack a required library for character type checking (php-ctype)', - 'ctype_is_ok' => 'You have the required library for character type checking (ctype)', - 'curl_is_nok' => 'You lack cURL (php5-curl package)', - 'curl_is_ok' => 'You have version %s of cURL', - 'current_user' => 'Current user', - 'damn' => 'Damn!', - 'data_is_ok' => 'Permissions on data directory are good', - 'dec' => 'dec', - 'december' => 'Dec', - 'default_category' => 'Uncategorized', - 'default_user' => 'Username of the default user (maximum 16 alphanumeric characters)', - 'default_view' => 'Default view', - 'delete' => 'Delete', - 'delete_articles_every' => 'Remove articles after', - 'diaspora' => 'Diaspora*', - 'display' => 'Display', - 'display_articles_unfolded' => 'Show articles unfolded by default', - 'display_categories_unfolded' => 'Show categories folded by default', - 'display_configuration' => 'Display', - 'do_not_change_if_doubt' => 'Don’t change if you doubt about it', - 'dom_is_nok' => 'You lack a required library to browse the DOM (php-xml package)', - 'dom_is_ok' => 'You have the required library to browse the DOM', - 'email' => 'Email', - 'error_occurred' => 'An error occurred', - 'error_occurred_update' => 'Nothing was changed', - 'explain_token' => 'Allows to access RSS output of the default user without authentication.
    %s?output=rss&token=%s', - 'export' => 'Export', - 'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.', - 'export_opml' => 'Export list of feeds (OPML)', - 'export_starred' => 'Export your favourites', - 'facebook' => 'Facebook', - 'favicons_is_ok' => 'Permissions on favicons directory are good', - 'favorite_feeds' => 'Favourites (%s)', - 'feb' => 'feb', - 'february' => 'Feb', - 'feed' => 'Feed', - 'feed_actualized' => '%s has been updated', - 'feed_added' => 'RSS feed %s has been added', - 'feed_deleted' => 'Feed has been deleted', - 'feed_description' => 'Description', - 'feed_empty' => 'This feed is empty. Please verify that it is still maintained.', - 'feed_in_error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', - 'feed_list' => 'List of %s articles', - 'feed_not_added' => '%s could not be added', - 'feed_updated' => 'Feed has been updated', - 'feed_url' => 'Feed URL', - 'feed_validator' => 'Check the validity of the feed', - 'feedback.login.error' => 'Login is invalid', - 'feedback.login.success' => 'You are connected', - 'feedback.logout.success' => 'You are disconnected', - 'feedback.user_profile.updated' => 'Your profile has been modified', - 'feeds' => 'Feeds', - 'feeds_actualized' => 'RSS feeds have been updated', - 'feeds_imported' => 'Your feeds have been imported and will now be updated', - 'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred', - 'feeds_marked_read' => 'Feeds have been marked as read', - 'feeds_moved_category_deleted' => 'When you delete a category, their feeds are automatically classified under %s.', - 'file_cannot_be_uploaded' => 'File cannot be uploaded!', - 'file_is_nok' => 'Check permissions on %s directory. HTTP server must have rights to write into', - 'file_to_import' => 'File to import
    (OPML, Json or Zip)', - 'file_to_import_no_zip' => 'File to import
    (OPML or Json)', - 'filter' => 'Filter', - 'finish_installation' => 'Complete installation', - 'first' => 'First', - 'first_article' => 'Skip to the first article', - 'fix_errors_before' => 'Fix errors before skip to the next step.', - 'focus_search' => 'Access search box', - 'format_date' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y', - 'format_date_hour' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y \\a\\t H\\:i', - 'freshrss' => 'FreshRSS', - 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like Kriss Feed or Leed. It is light and easy to take in hand while being powerful and configurable tool.', - 'freshrss_installation' => 'Installation · FreshRSS', - 'fri' => 'Fri', - 'g+' => 'Google+', - 'gen.menu.admin' => 'Administration', - 'gen.menu.authentication' => 'Authentication', - 'gen.menu.check_install' => 'Installation checking', - 'gen.menu.user_management' => 'Manage users', - 'gen.menu.user_profile' => 'Profile', - 'gen.title.authentication' => 'Authentication', - 'gen.title.check_install' => 'Installation checking', - 'gen.title.global_view' => 'Global view', - 'gen.title.user_management' => 'Manage users', - 'gen.title.user_profile' => 'Profile', - 'general_conf_is_ok' => 'General configuration has been saved.', - 'general_configuration' => 'General configuration', - 'github_or_email' => 'on Github or by mail', - 'global_view' => 'Global view', - 'help' => 'Display documentation', - 'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)', - 'host' => 'Host', - 'html5_notif_timeout' => 'HTML5 notification timeout', - 'http_auth' => 'HTTP (for advanced users with HTTPS)', - 'http_authentication' => 'HTTP Authentication', - 'http_password' => 'HTTP password', - 'http_referer_is_nok' => 'Please check that you are not altering your HTTP REFERER.', - 'http_referer_is_ok' => 'Your HTTP REFERER is known and corresponds to your server.', - 'http_username' => 'HTTP username', - 'img_with_lazyload' => 'Use "lazy load" mode to load pictures', - 'import' => 'Import', - 'import_export' => 'Import / export', - 'informations' => 'Information', - 'install_not_deleted' => 'Something went wrong; you must delete the file %s manually.', - 'installation_is_ok' => 'The installation process was successful.
    The final step will now attempt to delete any file and database backup created during the update process.
    You may choose to skip this step by deleting ./data/do-install.txt manually.', - 'installation_step' => 'Installation — step %d · FreshRSS', - 'internal_problem_feed' => 'The RSS feed could not be added. Check FressRSS logs for details.', - 'invalid_login' => 'Login is invalid', - 'invalid_url' => 'URL %s is invalid', - 'is_admin' => 'is administrator', - 'jan' => 'jan', - 'january' => 'Jan', - 'javascript_for_shortcuts' => 'JavaScript must be enabled in order to use shortcuts', - 'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled', - 'javascript_should_be_activated' => 'JavaScript must be enabled', - 'jul' => 'jul', - 'july' => 'Jul', - 'jump_next' => 'jump to next unread sibling (feed or category)', - 'jun' => 'jun', - 'june' => 'Jun', - 'keep_history' => 'Minimum number of articles to keep', - 'keep_logged_in' => 'Keep me logged in (1 month)', - 'language' => 'Language', - 'language_defined' => 'Language has been defined.', - 'last' => 'Last', - 'last_3_month' => 'Last three months', - 'last_6_month' => 'Last six months', - 'last_article' => 'Skip to the last article', - 'last_month' => 'Last month', - 'last_week' => 'Last week', - 'last_year' => 'Last year', - 'lead_developer' => 'Lead developer', - 'license' => 'License', - 'load_more' => 'Load more articles', - 'log_is_ok' => 'Permissions on logs directory are good', - 'login' => 'Login', - 'login_configuration' => 'Login', - 'login_persona_problem' => 'Connection problem with Persona?', - 'login_required' => 'Login required:', - 'login_with_persona' => 'Login with Persona', - 'logout' => 'Logout', - 'logs' => 'Logs', - 'logs_empty' => 'Log file is empty', - 'main_stream' => 'Main stream', - 'mar' => 'mar', - 'march' => 'Mar', - 'mark_all_read' => 'Mark all as read', - 'mark_cat_read' => 'Mark category as read', - 'mark_favorite' => 'Mark as favourite', - 'mark_feed_read' => 'Mark feed as read', - 'mark_read' => 'Mark as read', - 'may' => 'May', - 'minz_is_nok' => 'You lack the Minz framework. You should execute build.sh script or download it on Github and install in %s directory the content of its /lib directory.', - 'minz_is_ok' => 'You have the Minz framework', - 'mon' => 'Mon', - 'month' => 'months', - 'more_information' => 'More information', - 'n_entries_deleted' => '%d articles have been deleted', - 'n_feeds_actualized' => '%d feeds have been updated', - 'new_article' => 'There are new available articles, click to refresh the page.', - 'new_category' => 'New category', - 'newer_first' => 'Newer first', - 'next' => 'Next', - 'next_article' => 'Skip to the next article', - 'next_page' => 'Skip to the next page', - 'next_step' => 'Go to the next step', - 'no' => 'No', - 'no_feed_actualized' => 'No RSS feed has been updated', - 'no_feed_to_display' => 'There is no article to show.', - 'no_feed_to_refresh' => 'There is no feed to refresh…', - 'no_query' => 'You haven’t created any user query yet.', - 'no_query_filter' => 'No filter', - 'no_rss_feed' => 'No RSS feed', - 'no_selected_feed' => 'No feed selected.', - 'no_update' => 'No update to apply', - 'no_zip_extension' => 'Zip extension is not present on your server.', - 'normal_view' => 'Normal view', - 'not_read' => '%d unread', - 'not_reads' => '%d unread', - 'not_yet_implemented' => 'Not yet implemented', - 'nothing_to_load' => 'There are no more articles', - 'notif_body_new_articles' => 'There are \\d new articles to read on FreshRSS.', - 'notif_title_new_articles' => 'FreshRSS: new articles!', - 'nov' => 'nov', - 'november' => 'Nov', - 'number_articles' => '%d articles', - 'number_divided_when_reader' => 'Divided by 2 in the reading view.', - 'number_feeds' => '%d feeds', - 'oct' => 'oct', - 'october' => 'Oct', - 'ok' => 'Ok!', - 'older_first' => 'Oldest first', - 'oops' => 'Oops!', - 'optimization_complete' => 'Optimization complete', - 'optimize_bdd' => 'Optimize database', - 'optimize_todo_sometimes' => 'To do occasionally to reduce the size of the database', - 'or' => 'or', - 'page_not_found' => 'You are looking for a page which doesn’t exist', - 'password' => 'Password', - 'password_api' => 'Password API
    (e.g., for mobile apps)', - 'password_form' => 'Password
    (for the Web-form login method)', - 'pcre_is_nok' => 'You lack a required library for regular expressions (php-pcre)', - 'pcre_is_ok' => 'You have the required library for regular expressions (PCRE)', - 'pdo_is_nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite)', - 'pdo_is_ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite)', - 'persona_connection_email' => 'Login mail address
    (for Mozilla Persona)', - 'persona_is_ok' => 'Permissions on Mozilla Persona directory are good', - 'php_is_nok' => 'Your PHP version is %s but FreshRSS requires at least version %s', - 'php_is_ok' => 'Your PHP version is %s, which is compatible with FreshRSS', - 'prefix' => 'Table prefix', - 'previous' => 'Previous', - 'previous_article' => 'Skip to the previous article', - 'previous_page' => 'Skip to the previous page', - 'print' => 'Print', - 'project_website' => 'Project website', - 'public' => 'Public', - 'publication_date' => 'Date of publication', - 'purge_completed' => 'Purge completed (%d articles deleted)', - 'purge_now' => 'Purge now', - 'queries' => 'User queries', - 'query_created' => 'Query "%s" has been created.', - 'query_deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.', - 'query_filter' => 'Filter applied:', - 'query_get_all' => 'Display all articles', - 'query_get_category' => 'Display "%s" category', - 'query_get_favorite' => 'Display favorite articles', - 'query_get_feed' => 'Display "%s" feed', - 'query_number' => 'Query n°%d', - 'query_order_asc' => 'Display oldest articles first', - 'query_order_desc' => 'Display newest articles first', - 'query_search' => 'Search for "%s"', - 'query_state_0' => 'Display all articles', - 'query_state_1' => 'Display read articles', - 'query_state_2' => 'Display unread articles', - 'query_state_3' => 'Display all articles', - 'query_state_4' => 'Display favorite articles', - 'query_state_5' => 'Display read favorite articles', - 'query_state_6' => 'Display unread favorite articles', - 'query_state_7' => 'Display favorite articles', - 'query_state_8' => 'Display not favorite articles', - 'query_state_9' => 'Display read not favorite articles', - 'query_state_10' => 'Display unread not favorite articles', - 'query_state_11' => 'Display not favorite articles', - 'query_state_12' => 'Display all articles', - 'query_state_13' => 'Display read articles', - 'query_state_14' => 'Display unread articles', - 'query_state_15' => 'Display all articles', - 'random_string' => 'Random string', - 'reader_view' => 'Reading view', - 'reading_configuration' => 'Reading', - 'reading_confirm' => 'Display a confirmation dialog on “mark all as read” actions', - 'refresh' => 'Refresh', - 'related_tags' => 'Related tags', - 'retrieve_truncated_feeds' => 'Retrieves truncated RSS feeds (attention, requires more time!)', - 'rss_feed_management' => 'RSS feeds management', - 'rss_feeds_of' => 'RSS feed of %s', - 'rss_view' => 'RSS feed', - 'sat' => 'Sat', - 'save' => 'Save', - 'scroll' => 'while scrolling', - 'search' => 'Search words or #tags', - 'search_short' => 'Search', - 'seconds_(0_means_no_timeout)' => 'seconds (0 means no timeout)', - 'see_on_website' => 'See on original website', - 'see_website' => 'See website', - 'sep' => 'sep', - 'september' => 'Sep', - 'shaarli' => 'Shaarli', - 'share' => 'Share', - 'share_name' => 'Share name to display', - 'share_url' => 'Share URL to use', - 'sharing' => 'Sharing', - 'sharing_management' => 'Sharing options management', - 'shift_for_all_read' => '+ shift to mark all articles as read', - 'shortcuts' => 'Shortcuts', - 'shortcuts_article_action' => 'Article actions', - 'shortcuts_navigation' => 'Navigation', - 'shortcuts_navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.
    With the "Alt" modifier, navigation shortcuts apply on categories.', - 'shortcuts_other_action' => 'Other actions', - 'shortcuts_updated' => 'Shortcuts have been updated', - 'show_adaptive' => 'Adjust showing', - 'show_all_articles' => 'Show all articles', - 'show_favorite' => 'Show only favorites', - 'show_in_all_flux' => 'Show in main stream', - 'show_not_favorite' => 'Show all but favorites', - 'show_not_reads' => 'Show only unread', - 'show_read' => 'Show only read', - 'sort_order' => 'Sort order', - 'starred_list' => 'List of favourite articles', - 'stats' => 'Statistics', - 'stats_entry_count' => 'Entry count', - 'stats_entry_per_category' => 'Entries per category', - 'stats_entry_per_day' => 'Entries per day (last 30 days)', - 'stats_entry_per_day_of_week' => 'Per day of week (average: %.2f messages)', - 'stats_entry_per_hour' => 'Per hour (average: %.2f messages)', - 'stats_entry_per_month' => 'Per month (average: %.2f messages)', - 'stats_entry_repartition' => 'Entries repartition', - 'stats_feed_per_category' => 'Feeds per category', - 'stats_idle' => 'Idle feeds', - 'stats_main' => 'Main statistics', - 'stats_no_idle' => 'There is no idle feed!', - 'stats_percent_of_total' => '%% of total', - 'stats_repartition' => 'Articles repartition', - 'stats_top_feed' => 'Top ten feeds', - 'status_favorites' => 'Favourites', - 'status_read' => 'Read', - 'status_total' => 'Total', - 'status_unread' => 'Unread', - 'steps' => 'Steps', - 'sticky_post' => 'Stick the article to the top when opened', - 'sub.categories.over_max' => 'You have reached your limit of categories (%d)', - 'sub.feeds.over_max' => 'You have reached your limit of feeds (%d)', - 'submit' => 'Submit', - 'subscription_management' => 'Subscriptions management', - 'sun' => 'Sun', - 'theme' => 'Theme', - 'think_to_add' => 'You may add some feeds.', - 'this_is_the_end' => 'This is the end', - 'thu' => 'Thu', - 'title' => 'Title', - 'today' => 'Today', - 'top_line' => 'Top line', - 'truncate' => 'Delete all articles', - 'ttl' => 'Do not automatically refresh more often than', - 'tue' => 'Tue', - 'twitter' => 'Twitter', - 'unsafe_autologin' => 'Allow unsafe automatic login using the format: ', - 'update' => 'Update', - 'update_apply' => 'Apply', - 'update_can_apply' => 'An update is available.', - 'update_check' => 'Check for new updates', - 'update_end' => 'Update process is completed, now you can go to the final step.', - 'update_finished' => 'Update completed!', - 'update_last' => 'Last verification: %s', - 'update_long' => 'This can take a long time, depending on the size of your database. You may have to wait for this page to time out (~5 minutes) and then refresh this page.', - 'update_problem' => 'The update process has encountered an error: %s', - 'update_server_not_found' => 'Update server cannot be found. [%s]', - 'update_start' => 'Start update process', - 'update_system' => 'Update system', - 'updated' => 'Modifications have been updated', - 'upon_reception' => 'upon reception of the article', - 'user_created' => 'User %s has been created', - 'user_deleted' => 'User %s has been deleted', - 'user_filter' => 'Access user filters', - 'user_filter_help' => 'If there is only one user filter, it is used. Else filters are accessible by their number.', - 'username' => 'Username', - 'username_admin' => 'Administrator username', - 'users' => 'Users', - 'users_list' => 'List of users', - 'version' => 'Version', - 'version_update' => 'Update', - 'wallabag' => 'wallabag', - 'website' => 'Website', - 'website_url' => 'Website URL', - 'wed' => 'Wed', - 'width_large' => 'Large', - 'width_medium' => 'Medium', - 'width_no_limit' => 'No limit', - 'width_thin' => 'Thin', - 'yes' => 'Yes', - 'yesterday' => 'Yesterday', - 'your_diaspora_pod' => 'Your Diaspora* pod', - 'your_favorites' => 'Your favourites', - 'your_rss_feeds' => 'Your RSS feeds', - 'your_shaarli' => 'Your Shaarli', - 'your_wallabag' => 'Your wallabag', - 'zip_error' => 'An error occured during Zip import.', -); \ No newline at end of file diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php new file mode 100644 index 000000000..74f01ae06 --- /dev/null +++ b/app/i18n/en/admin.php @@ -0,0 +1,92 @@ + array( + 'cache' => array( + 'nok' => 'Check permissions on ./data/cache directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on cache directory are good.', + ), + 'categories' => array( + 'nok' => 'Category table is bad configured.', + 'ok' => 'Category table is ok.', + ), + 'connection' => array( + 'nok' => 'Connection to the database cannot being established.', + 'ok' => 'Connection to the database is ok.', + ), + 'ctype' => array( + 'nok' => 'You lack a required library for character type checking (php-ctype).', + 'ok' => 'You have the required library for character type checking (ctype).', + ), + 'curl' => array( + 'nok' => 'You lack cURL (php5-curl package).', + 'ok' => 'You have cURL extension.', + ), + 'data' => array( + 'nok' => 'Check permissions on ./data directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on data directory are good.', + ), + 'database' => 'Database installation', + 'dom' => array( + 'nok' => 'You lack a required library to browse the DOM (php-xml package).', + 'ok' => 'You have the required library to browse the DOM.', + ), + 'entries' => array( + 'nok' => 'Entry table is bad configured.', + 'ok' => 'Entry table is ok.', + ), + 'favicons' => array( + 'nok' => 'Check permissions on ./data/favicons directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on favicons directory are good.', + ), + 'feeds' => array( + 'nok' => 'Feed table is bad configured.', + 'ok' => 'Feed table is ok.', + ), + 'files' => 'File installation', + 'json' => array( + 'nok' => 'You lack JSON (php5-json package).', + 'ok' => 'You have JSON extension.', + ), + 'logs' => array( + 'nok' => 'Check permissions on ./data/logs directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on logs directory are good.', + ), + 'minz' => array( + 'nok' => 'You lack the Minz framework.', + 'ok' => 'You have the Minz framework.', + ), + 'pcre' => array( + 'nok' => 'You lack a required library for regular expressions (php-pcre).', + 'ok' => 'You have the required library for regular expressions (PCRE).', + ), + 'pdo' => array( + 'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).', + 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Check permissions on ./data/persona directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on Mozilla Persona directory are good.', + ), + 'php' => array( + '_' => 'PHP installation', + 'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.', + 'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.', + ), + 'tables' => array( + 'nok' => 'There is one or more lacking tables in the database.', + 'ok' => 'Tables are existing in the database.', + ), + 'tokens' => array( + 'nok' => 'Check permissions on ./data/tokens directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on tokens directory are good.', + ), + 'zip' => array( + 'nok' => 'You lack ZIP extension (php5-zip package).', + 'ok' => 'You have ZIP extension.', + ), + ), + 'users' => array( + 'articles_and_size' => '%s articles (%s)', + ), +); diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php new file mode 100644 index 000000000..460804774 --- /dev/null +++ b/app/i18n/en/conf.php @@ -0,0 +1,7 @@ + array( + 'articles_and_size' => '%s articles (%s)', + ), +); diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php new file mode 100644 index 000000000..b3866f1dc --- /dev/null +++ b/app/i18n/en/feedback.php @@ -0,0 +1,14 @@ + array( + 'error' => 'Login is invalid', + 'success' => 'You are connected', + ), + 'logout' => array( + 'success' => 'You are disconnected', + ), + 'user_profile' => array( + 'updated' => 'Your profile has been modified', + ), +); diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php new file mode 100644 index 000000000..9e06357bc --- /dev/null +++ b/app/i18n/en/gen.php @@ -0,0 +1,487 @@ + '\\A\\p\\r\\i\\l', + 'Aug' => '\\A\\u\\g\\u\\s\\t', + 'Dec' => '\\D\\e\\c\\e\\m\\b\\e\\r', + 'Feb' => '\\F\\e\\b\\r\\u\\a\\r\\y', + 'Jan' => '\\J\\a\\n\\u\\a\\r\\y', + 'Jul' => '\\J\\u\\l\\y', + 'Jun' => '\\J\\u\\n\\e', + 'Mar' => '\\M\\a\\r\\c\\h', + 'May' => '\\M\\a\\y', + 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r', + 'Oct' => '\\O\\c\\t\\o\\b\\e\\r', + 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r', + 'about' => 'About', + 'about_freshrss' => 'About FreshRSS', + 'access_denied' => 'You don’t have permission to access this page', + 'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds', + 'activate_sharing' => 'Activate sharing', + 'actualize' => 'Actualize', + 'add_category' => 'Add a category', + 'add_query' => 'Add a query', + 'add_rss_feed' => 'Add a RSS feed', + 'administration' => 'Manage', + 'advanced' => 'Advanced', + 'after_onread' => 'After “mark all as read”,', + 'agpl3' => 'AGPL 3', + 'all_feeds' => 'All feeds', + 'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)', + 'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles', + 'already_subscribed' => 'You have already subscribed to %s', + 'api_enabled' => 'Allow API access (required for mobile apps)', + 'apr' => 'apr', + 'april' => 'Apr', + 'archiving_configuration' => 'Archiving', + 'archiving_configuration_help' => 'More options are available in the individual stream settings', + 'article' => 'Article', + 'article_icons' => 'Article icons', + 'article_open_on_website' => 'when article is opened on its original website', + 'article_published_on' => 'This article originally appeared on %s', + 'article_published_on_author' => 'This article originally appeared on %s by %s', + 'article_viewed' => 'when article is viewed', + 'articles' => 'articles', + 'articles_per_page' => 'Number of articles per page', + 'articles_to_display' => 'Articles to display', + 'ask_empty' => 'Clear?', + 'attention' => 'Attention!', + 'aug' => 'aug', + 'august' => 'Aug', + 'auth_form' => 'Web form (traditional, requires JavaScript)', + 'auth_form_not_set' => 'A problem occured during authentication system configuration. Please retry later.', + 'auth_form_set' => 'Form is now your default authentication system.', + 'auth_no_password_set' => 'Administrator password hasn’t been set. This feature isn’t available.', + 'auth_none' => 'None (dangerous)', + 'auth_not_persona' => 'Only Persona system can be reset.', + 'auth_persona' => 'Mozilla Persona (modern, requires JavaScript)', + 'auth_reset' => 'Authentication reset', + 'auth_token' => 'Authentication token', + 'auth_type' => 'Authentication method', + 'auth_will_reset' => 'Authentication system will be reset: a form will be used instead of Persona.', + 'author' => 'Author', + 'auto_load_more' => 'Load next articles at the page bottom', + 'auto_read_when' => 'Mark article as read…', + 'auto_share' => 'Share', + 'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.', + 'back_to_rss_feeds' => '← Go back to your RSS feeds', + 'bad_opml_file' => 'Your OPML file is invalid', + 'base_url' => 'Base URL', + 'bdd' => 'Database', + 'bdd_conf_is_ko' => 'Verify your database information.', + 'bdd_conf_is_ok' => 'Database configuration has been saved.', + 'bdd_configuration' => 'Database configuration', + 'bdd_type' => 'Type of database', + 'before_one_day' => 'Before one day', + 'before_one_week' => 'Before one week', + 'before_yesterday' => 'Before yesterday', + 'blank_to_disable' => 'Leave blank to disable', + 'blogotext' => 'Blogotext', + 'bookmark' => 'Subscribe (FreshRSS bookmark)', + 'bottom_line' => 'Bottom line', + 'bugs_reports' => 'Bugs reports', + 'by' => 'by', + 'by_author' => 'By %s', + 'by_default' => 'By default', + 'by_email' => 'By email', + 'by_feed' => 'by feed', + 'cache_is_ok' => 'Permissions on cache directory are good', + 'can_not_be_deleted' => 'Cannot be deleted', + 'cancel' => 'Cancel', + 'categories' => 'Categories', + 'categories_management' => 'Categories management', + 'categories_updated' => 'Categories have been updated', + 'categorize' => 'Store in a category', + 'category' => 'Category', + 'category_created' => 'Category %s has been created.', + 'category_deleted' => 'Category has been deleted.', + 'category_emptied' => 'Category has been emptied', + 'category_empty' => 'Empty category', + 'category_name_exists' => 'Category name already exists.', + 'category_no_id' => 'You must precise the id of the category.', + 'category_no_name' => 'Category name cannot be empty.', + 'category_not_delete_default' => 'You cannot delete the default category!', + 'category_not_exist' => 'The category does not exist!', + 'category_number' => 'Category n°%d', + 'category_updated' => 'Category has been updated.', + 'change_value' => 'You should change this value by any other', + 'checks' => 'Checks', + 'choose_language' => 'Choose a language for FreshRSS', + 'clear_logs' => 'Clear the logs', + 'collapse_article' => 'Collapse', + 'configuration' => 'Configuration', + 'configuration_updated' => 'Configuration has been updated', + 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', + 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!', + 'congratulations' => 'Congratulations!', + 'content_width' => 'Content width', + 'create' => 'Create', + 'create_user' => 'Create new user', + 'credits' => 'Credits', + 'credits_content' => 'Some design elements come from Bootstrap although FreshRSS doesn’t use this framework. Icons come from GNOME project. Open Sans font police has been created by Steve Matteson. Favicons are collected with getFavicon API. FreshRSS is based on Minz, a PHP framework.', + 'css_path_on_website' => 'Articles CSS path on original website', + 'ctype_is_nok' => 'You lack a required library for character type checking (php-ctype)', + 'ctype_is_ok' => 'You have the required library for character type checking (ctype)', + 'curl_is_nok' => 'You lack cURL (php5-curl package)', + 'curl_is_ok' => 'You have version %s of cURL', + 'current_user' => 'Current user', + 'damn' => 'Damn!', + 'data_is_ok' => 'Permissions on data directory are good', + 'dec' => 'dec', + 'december' => 'Dec', + 'default_category' => 'Uncategorized', + 'default_user' => 'Username of the default user (maximum 16 alphanumeric characters)', + 'default_view' => 'Default view', + 'delete' => 'Delete', + 'delete_articles_every' => 'Remove articles after', + 'diaspora' => 'Diaspora*', + 'display' => 'Display', + 'display_articles_unfolded' => 'Show articles unfolded by default', + 'display_categories_unfolded' => 'Show categories folded by default', + 'display_configuration' => 'Display', + 'do_not_change_if_doubt' => 'Don’t change if you doubt about it', + 'dom_is_nok' => 'You lack a required library to browse the DOM (php-xml package)', + 'dom_is_ok' => 'You have the required library to browse the DOM', + 'email' => 'Email', + 'error_occurred' => 'An error occurred', + 'error_occurred_update' => 'Nothing was changed', + 'explain_token' => 'Allows to access RSS output of the default user without authentication.
    %s?output=rss&token=%s', + 'export' => 'Export', + 'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.', + 'export_opml' => 'Export list of feeds (OPML)', + 'export_starred' => 'Export your favourites', + 'facebook' => 'Facebook', + 'favicons_is_ok' => 'Permissions on favicons directory are good', + 'favorite_feeds' => 'Favourites (%s)', + 'feb' => 'feb', + 'february' => 'Feb', + 'feed' => 'Feed', + 'feed_actualized' => '%s has been updated', + 'feed_added' => 'RSS feed %s has been added', + 'feed_deleted' => 'Feed has been deleted', + 'feed_description' => 'Description', + 'feed_empty' => 'This feed is empty. Please verify that it is still maintained.', + 'feed_in_error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', + 'feed_list' => 'List of %s articles', + 'feed_not_added' => '%s could not be added', + 'feed_updated' => 'Feed has been updated', + 'feed_url' => 'Feed URL', + 'feed_validator' => 'Check the validity of the feed', + 'feeds' => 'Feeds', + 'feeds_actualized' => 'RSS feeds have been updated', + 'feeds_imported' => 'Your feeds have been imported and will now be updated', + 'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred', + 'feeds_marked_read' => 'Feeds have been marked as read', + 'feeds_moved_category_deleted' => 'When you delete a category, their feeds are automatically classified under %s.', + 'file_cannot_be_uploaded' => 'File cannot be uploaded!', + 'file_is_nok' => 'Check permissions on %s directory. HTTP server must have rights to write into', + 'file_to_import' => 'File to import
    (OPML, Json or Zip)', + 'file_to_import_no_zip' => 'File to import
    (OPML or Json)', + 'filter' => 'Filter', + 'finish_installation' => 'Complete installation', + 'first' => 'First', + 'first_article' => 'Skip to the first article', + 'fix_errors_before' => 'Fix errors before skip to the next step.', + 'focus_search' => 'Access search box', + 'format_date' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y', + 'format_date_hour' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y \\a\\t H\\:i', + 'freshrss' => 'FreshRSS', + 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like Kriss Feed or Leed. It is light and easy to take in hand while being powerful and configurable tool.', + 'freshrss_installation' => 'Installation · FreshRSS', + 'fri' => 'Fri', + 'g+' => 'Google+', + 'menu' => array( + 'admin' => 'Administration', + 'authentication' => 'Authentication', + 'check_install' => 'Installation checking', + 'user_management' => 'Manage users', + 'user_profile' => 'Profile', + ), + 'title' => array( + '_' => 'Title', + 'authentication' => 'Authentication', + 'check_install' => 'Installation checking', + 'global_view' => 'Global view', + 'user_management' => 'Manage users', + 'user_profile' => 'Profile', + ), + 'general_conf_is_ok' => 'General configuration has been saved.', + 'general_configuration' => 'General configuration', + 'github_or_email' => 'on Github or by mail', + 'global_view' => 'Global view', + 'help' => 'Display documentation', + 'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)', + 'host' => 'Host', + 'html5_notif_timeout' => 'HTML5 notification timeout', + 'http_auth' => 'HTTP (for advanced users with HTTPS)', + 'http_authentication' => 'HTTP Authentication', + 'http_password' => 'HTTP password', + 'http_referer_is_nok' => 'Please check that you are not altering your HTTP REFERER.', + 'http_referer_is_ok' => 'Your HTTP REFERER is known and corresponds to your server.', + 'http_username' => 'HTTP username', + 'img_with_lazyload' => 'Use "lazy load" mode to load pictures', + 'import' => 'Import', + 'import_export' => 'Import / export', + 'informations' => 'Information', + 'install_not_deleted' => 'Something went wrong; you must delete the file %s manually.', + 'installation_is_ok' => 'The installation process was successful.
    The final step will now attempt to delete any file and database backup created during the update process.
    You may choose to skip this step by deleting ./data/do-install.txt manually.', + 'installation_step' => 'Installation — step %d · FreshRSS', + 'internal_problem_feed' => 'The RSS feed could not be added. Check FressRSS logs for details.', + 'invalid_login' => 'Login is invalid', + 'invalid_url' => 'URL %s is invalid', + 'is_admin' => 'is administrator', + 'jan' => 'jan', + 'january' => 'Jan', + 'javascript_for_shortcuts' => 'JavaScript must be enabled in order to use shortcuts', + 'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled', + 'javascript_should_be_activated' => 'JavaScript must be enabled', + 'jul' => 'jul', + 'july' => 'Jul', + 'jump_next' => 'jump to next unread sibling (feed or category)', + 'jun' => 'jun', + 'june' => 'Jun', + 'keep_history' => 'Minimum number of articles to keep', + 'keep_logged_in' => 'Keep me logged in (1 month)', + 'language' => 'Language', + 'language_defined' => 'Language has been defined.', + 'last' => 'Last', + 'last_3_month' => 'Last three months', + 'last_6_month' => 'Last six months', + 'last_article' => 'Skip to the last article', + 'last_month' => 'Last month', + 'last_week' => 'Last week', + 'last_year' => 'Last year', + 'lead_developer' => 'Lead developer', + 'license' => 'License', + 'load_more' => 'Load more articles', + 'log_is_ok' => 'Permissions on logs directory are good', + 'login' => 'Login', + 'login_configuration' => 'Login', + 'login_persona_problem' => 'Connection problem with Persona?', + 'login_required' => 'Login required:', + 'login_with_persona' => 'Login with Persona', + 'logout' => 'Logout', + 'logs' => 'Logs', + 'logs_empty' => 'Log file is empty', + 'main_stream' => 'Main stream', + 'mar' => 'mar', + 'march' => 'Mar', + 'mark_all_read' => 'Mark all as read', + 'mark_cat_read' => 'Mark category as read', + 'mark_favorite' => 'Mark as favourite', + 'mark_feed_read' => 'Mark feed as read', + 'mark_read' => 'Mark as read', + 'may' => 'May', + 'minz_is_nok' => 'You lack the Minz framework. You should execute build.sh script or download it on Github and install in %s directory the content of its /lib directory.', + 'minz_is_ok' => 'You have the Minz framework', + 'mon' => 'Mon', + 'month' => 'months', + 'more_information' => 'More information', + 'n_entries_deleted' => '%d articles have been deleted', + 'n_feeds_actualized' => '%d feeds have been updated', + 'new_article' => 'There are new available articles, click to refresh the page.', + 'new_category' => 'New category', + 'newer_first' => 'Newer first', + 'next' => 'Next', + 'next_article' => 'Skip to the next article', + 'next_page' => 'Skip to the next page', + 'next_step' => 'Go to the next step', + 'no' => 'No', + 'no_feed_actualized' => 'No RSS feed has been updated', + 'no_feed_to_display' => 'There is no article to show.', + 'no_feed_to_refresh' => 'There is no feed to refresh…', + 'no_query' => 'You haven’t created any user query yet.', + 'no_query_filter' => 'No filter', + 'no_rss_feed' => 'No RSS feed', + 'no_selected_feed' => 'No feed selected.', + 'no_update' => 'No update to apply', + 'no_zip_extension' => 'Zip extension is not present on your server.', + 'normal_view' => 'Normal view', + 'not_read' => '%d unread', + 'not_reads' => '%d unread', + 'not_yet_implemented' => 'Not yet implemented', + 'nothing_to_load' => 'There are no more articles', + 'notif_body_new_articles' => 'There are \\d new articles to read on FreshRSS.', + 'notif_title_new_articles' => 'FreshRSS: new articles!', + 'nov' => 'nov', + 'november' => 'Nov', + 'number_articles' => '%d articles', + 'number_divided_when_reader' => 'Divided by 2 in the reading view.', + 'number_feeds' => '%d feeds', + 'oct' => 'oct', + 'october' => 'Oct', + 'ok' => 'Ok!', + 'older_first' => 'Oldest first', + 'oops' => 'Oops!', + 'optimization_complete' => 'Optimization complete', + 'optimize_bdd' => 'Optimize database', + 'optimize_todo_sometimes' => 'To do occasionally to reduce the size of the database', + 'or' => 'or', + 'page_not_found' => 'You are looking for a page which doesn’t exist', + 'password' => 'Password', + 'password_api' => 'Password API
    (e.g., for mobile apps)', + 'password_form' => 'Password
    (for the Web-form login method)', + 'pcre_is_nok' => 'You lack a required library for regular expressions (php-pcre)', + 'pcre_is_ok' => 'You have the required library for regular expressions (PCRE)', + 'pdo_is_nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite)', + 'pdo_is_ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite)', + 'persona_connection_email' => 'Login mail address
    (for Mozilla Persona)', + 'persona_is_ok' => 'Permissions on Mozilla Persona directory are good', + 'php_is_nok' => 'Your PHP version is %s but FreshRSS requires at least version %s', + 'php_is_ok' => 'Your PHP version is %s, which is compatible with FreshRSS', + 'prefix' => 'Table prefix', + 'previous' => 'Previous', + 'previous_article' => 'Skip to the previous article', + 'previous_page' => 'Skip to the previous page', + 'print' => 'Print', + 'project_website' => 'Project website', + 'public' => 'Public', + 'publication_date' => 'Date of publication', + 'purge_completed' => 'Purge completed (%d articles deleted)', + 'purge_now' => 'Purge now', + 'queries' => 'User queries', + 'query_created' => 'Query "%s" has been created.', + 'query_deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.', + 'query_filter' => 'Filter applied:', + 'query_get_all' => 'Display all articles', + 'query_get_category' => 'Display "%s" category', + 'query_get_favorite' => 'Display favorite articles', + 'query_get_feed' => 'Display "%s" feed', + 'query_number' => 'Query n°%d', + 'query_order_asc' => 'Display oldest articles first', + 'query_order_desc' => 'Display newest articles first', + 'query_search' => 'Search for "%s"', + 'query_state_0' => 'Display all articles', + 'query_state_1' => 'Display read articles', + 'query_state_2' => 'Display unread articles', + 'query_state_3' => 'Display all articles', + 'query_state_4' => 'Display favorite articles', + 'query_state_5' => 'Display read favorite articles', + 'query_state_6' => 'Display unread favorite articles', + 'query_state_7' => 'Display favorite articles', + 'query_state_8' => 'Display not favorite articles', + 'query_state_9' => 'Display read not favorite articles', + 'query_state_10' => 'Display unread not favorite articles', + 'query_state_11' => 'Display not favorite articles', + 'query_state_12' => 'Display all articles', + 'query_state_13' => 'Display read articles', + 'query_state_14' => 'Display unread articles', + 'query_state_15' => 'Display all articles', + 'random_string' => 'Random string', + 'reader_view' => 'Reading view', + 'reading_configuration' => 'Reading', + 'reading_confirm' => 'Display a confirmation dialog on “mark all as read” actions', + 'refresh' => 'Refresh', + 'related_tags' => 'Related tags', + 'retrieve_truncated_feeds' => 'Retrieves truncated RSS feeds (attention, requires more time!)', + 'rss_feed_management' => 'RSS feeds management', + 'rss_feeds_of' => 'RSS feed of %s', + 'rss_view' => 'RSS feed', + 'sat' => 'Sat', + 'save' => 'Save', + 'scroll' => 'while scrolling', + 'search' => 'Search words or #tags', + 'search_short' => 'Search', + 'seconds_(0_means_no_timeout)' => 'seconds (0 means no timeout)', + 'see_on_website' => 'See on original website', + 'see_website' => 'See website', + 'sep' => 'sep', + 'september' => 'Sep', + 'shaarli' => 'Shaarli', + 'share' => 'Share', + 'share_name' => 'Share name to display', + 'share_url' => 'Share URL to use', + 'sharing' => 'Sharing', + 'sharing_management' => 'Sharing options management', + 'shift_for_all_read' => '+ shift to mark all articles as read', + 'shortcuts' => 'Shortcuts', + 'shortcuts_article_action' => 'Article actions', + 'shortcuts_navigation' => 'Navigation', + 'shortcuts_navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.
    With the "Alt" modifier, navigation shortcuts apply on categories.', + 'shortcuts_other_action' => 'Other actions', + 'shortcuts_updated' => 'Shortcuts have been updated', + 'show_adaptive' => 'Adjust showing', + 'show_all_articles' => 'Show all articles', + 'show_favorite' => 'Show only favorites', + 'show_in_all_flux' => 'Show in main stream', + 'show_not_favorite' => 'Show all but favorites', + 'show_not_reads' => 'Show only unread', + 'show_read' => 'Show only read', + 'sort_order' => 'Sort order', + 'starred_list' => 'List of favourite articles', + 'stats' => 'Statistics', + 'stats_entry_count' => 'Entry count', + 'stats_entry_per_category' => 'Entries per category', + 'stats_entry_per_day' => 'Entries per day (last 30 days)', + 'stats_entry_per_day_of_week' => 'Per day of week (average: %.2f messages)', + 'stats_entry_per_hour' => 'Per hour (average: %.2f messages)', + 'stats_entry_per_month' => 'Per month (average: %.2f messages)', + 'stats_entry_repartition' => 'Entries repartition', + 'stats_feed_per_category' => 'Feeds per category', + 'stats_idle' => 'Idle feeds', + 'stats_main' => 'Main statistics', + 'stats_no_idle' => 'There is no idle feed!', + 'stats_percent_of_total' => '%% of total', + 'stats_repartition' => 'Articles repartition', + 'stats_top_feed' => 'Top ten feeds', + 'status_favorites' => 'Favourites', + 'status_read' => 'Read', + 'status_total' => 'Total', + 'status_unread' => 'Unread', + 'steps' => 'Steps', + 'sticky_post' => 'Stick the article to the top when opened', + 'submit' => 'Submit', + 'subscription_management' => 'Subscriptions management', + 'sun' => 'Sun', + 'theme' => 'Theme', + 'think_to_add' => 'You may add some feeds.', + 'this_is_the_end' => 'This is the end', + 'thu' => 'Thu', + 'today' => 'Today', + 'top_line' => 'Top line', + 'truncate' => 'Delete all articles', + 'ttl' => 'Do not automatically refresh more often than', + 'tue' => 'Tue', + 'twitter' => 'Twitter', + 'unsafe_autologin' => 'Allow unsafe automatic login using the format: ', + 'update' => 'Update', + 'update_apply' => 'Apply', + 'update_can_apply' => 'An update is available.', + 'update_check' => 'Check for new updates', + 'update_end' => 'Update process is completed, now you can go to the final step.', + 'update_finished' => 'Update completed!', + 'update_last' => 'Last verification: %s', + 'update_long' => 'This can take a long time, depending on the size of your database. You may have to wait for this page to time out (~5 minutes) and then refresh this page.', + 'update_problem' => 'The update process has encountered an error: %s', + 'update_server_not_found' => 'Update server cannot be found. [%s]', + 'update_start' => 'Start update process', + 'update_system' => 'Update system', + 'updated' => 'Modifications have been updated', + 'upon_reception' => 'upon reception of the article', + 'user_created' => 'User %s has been created', + 'user_deleted' => 'User %s has been deleted', + 'user_filter' => 'Access user filters', + 'user_filter_help' => 'If there is only one user filter, it is used. Else filters are accessible by their number.', + 'username' => 'Username', + 'username_admin' => 'Administrator username', + 'users' => 'Users', + 'users_list' => 'List of users', + 'version' => 'Version', + 'version_update' => 'Update', + 'wallabag' => 'wallabag', + 'website' => 'Website', + 'website_url' => 'Website URL', + 'wed' => 'Wed', + 'width_large' => 'Large', + 'width_medium' => 'Medium', + 'width_no_limit' => 'No limit', + 'width_thin' => 'Thin', + 'yes' => 'Yes', + 'yesterday' => 'Yesterday', + 'your_diaspora_pod' => 'Your Diaspora* pod', + 'your_favorites' => 'Your favourites', + 'your_rss_feeds' => 'Your RSS feeds', + 'your_shaarli' => 'Your Shaarli', + 'your_wallabag' => 'Your wallabag', + 'zip_error' => 'An error occured during Zip import.', +); diff --git a/app/i18n/en/index.php b/app/i18n/en/index.php new file mode 100644 index 000000000..afca37ed3 --- /dev/null +++ b/app/i18n/en/index.php @@ -0,0 +1,5 @@ + array( + 'over_max' => 'You have reached your limit of categories (%d)', + ), + 'feeds' => array( + 'over_max' => 'You have reached your limit of feeds (%d)', + ), +); diff --git a/app/i18n/fr.php b/app/i18n/fr.php deleted file mode 100644 index c29b6c9ac..000000000 --- a/app/i18n/fr.php +++ /dev/null @@ -1,533 +0,0 @@ - '\\a\\v\\r\\i\\l', - 'Aug' => '\\a\\o\\û\\t', - 'Dec' => '\\d\\é\\c\\e\\m\\b\\r\\e', - 'Feb' => '\\f\\é\\v\\r\\i\\e\\r', - 'Jan' => '\\j\\a\\n\\v\\i\\e\\r', - 'Jul' => '\\j\\u\\i\\l\\l\\e\\t', - 'Jun' => '\\j\\u\\i\\n', - 'Mar' => '\\m\\a\\r\\s', - 'May' => '\\m\\a\\i', - 'Nov' => '\\n\\o\\v\\e\\m\\b\\r\\e', - 'Oct' => '\\o\\c\\t\\o\\b\\r\\e', - 'Sep' => '\\s\\e\\p\\t\\e\\m\\b\\r\\e', - 'about' => 'À propos', - 'about_freshrss' => 'À propos de FreshRSS', - 'access_denied' => 'Vous n’avez pas le droit d’accéder à cette page !', - 'access_protected_feeds' => 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.', - 'activate_sharing' => 'Activer le partage', - 'actualize' => 'Actualiser', - 'add_category' => 'Ajouter une catégorie', - 'add_query' => 'Créer un filtre', - 'add_rss_feed' => 'Ajouter un flux RSS', - 'admin.check_install.cache.nok' => 'Veuillez vérifier les droits sur le répertoire ./data/cache. Le serveur HTTP doit être capable d’écrire dedans', - 'admin.check_install.cache.ok' => 'Les droits sur le répertoire de cache sont bons.', - 'admin.check_install.categories.nok' => 'La table category est mal configurée.', - 'admin.check_install.categories.ok' => 'La table category est bien configurée.', - 'admin.check_install.connection.nok' => 'La connexion à la base de données est impossible.', - 'admin.check_install.connection.ok' => 'La connexion à la base de données est bonne.', - 'admin.check_install.ctype.nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).', - 'admin.check_install.ctype.ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).', - 'admin.check_install.curl.nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).', - 'admin.check_install.curl.ok' => 'Vous disposez de cURL.', - 'admin.check_install.data.nok' => 'Veuillez vérifier les droits sur le répertoire ./data. Le serveur HTTP doit être capable d’écrire dedans', - 'admin.check_install.data.ok' => 'Les droits sur le répertoire de data sont bons.', - 'admin.check_install.database' => 'Installation de la base de données', - 'admin.check_install.dom.nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml).', - 'admin.check_install.dom.ok' => 'Vous disposez du nécessaire pour parcourir le DOM.', - 'admin.check_install.entries.nok' => 'La table entry est mal configurée.', - 'admin.check_install.entries.ok' => 'La table entry est bien configurée.', - 'admin.check_install.favicons.nok' => 'Veuillez vérifier les droits sur le répertoire ./data/favicons. Le serveur HTTP doit être capable d’écrire dedans', - 'admin.check_install.favicons.ok' => 'Les droits sur le répertoire des favicons sont bons.', - 'admin.check_install.feeds.nok' => 'La table feed est mal configurée.', - 'admin.check_install.feeds.ok' => 'La table feed est bien configurée.', - 'admin.check_install.files' => 'Installation des fichiers', - 'admin.check_install.json.nok' => 'Vous ne disposez pas de JSON (paquet php5-json).', - 'admin.check_install.json.ok' => 'Vous disposez de l\'extension JSON.', - 'admin.check_install.logs.nok' => 'Veuillez vérifier les droits sur le répertoire ./data/logs. Le serveur HTTP doit être capable d’écrire dedans', - 'admin.check_install.logs.ok' => 'Les droits sur le répertoire des logs sont bons.', - 'admin.check_install.minz.nok' => 'Vous ne disposez pas de la librairie Minz.', - 'admin.check_install.minz.ok' => 'Vous disposez du framework Minz', - 'admin.check_install.pcre.nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).', - 'admin.check_install.pcre.ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).', - 'admin.check_install.pdo.nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).', - 'admin.check_install.pdo.ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).', - 'admin.check_install.persona.nok' => 'Veuillez vérifier les droits sur le répertoire ./data/persona. Le serveur HTTP doit être capable d’écrire dedans', - 'admin.check_install.persona.ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons.', - 'admin.check_install.php' => 'Installation de PHP', - 'admin.check_install.php.nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.', - 'admin.check_install.php.ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.', - 'admin.check_install.tables.nok' => 'Il manque une ou plusieurs tables en base de données.', - 'admin.check_install.tables.ok' => 'Les tables sont bien présentes en base de données.', - 'admin.check_install.tokens.nok' => 'Veuillez vérifier les droits sur le répertoire ./data/tokens. Le serveur HTTP doit être capable d’écrire dedans', - 'admin.check_install.tokens.ok' => 'Les droits sur le répertoire des tokens sont bons.', - 'admin.check_install.zip.nok' => 'Vous ne disposez pas de l\'extension ZIP (paquet php5-zip).', - 'admin.check_install.zip.ok' => 'Vous disposez de l\'extension ZIP.', - 'admin.users.articles_and_size' => '%s articles (%s)', - 'administration' => 'Gérer', - 'advanced' => 'Avancé', - 'after_onread' => 'Après “marquer tout comme lu”,', - 'agpl3' => 'AGPL 3', - 'all_feeds' => 'Tous les flux', - 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', - 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', - 'already_subscribed' => 'Vous êtes déjà abonné à %s', - 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applis mobiles)', - 'apr' => 'avr.', - 'april' => 'avril', - 'archiving_configuration' => 'Archivage', - 'archiving_configuration_help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.', - 'article' => 'Article', - 'article_icons' => 'Icônes d’article', - 'article_open_on_website' => 'lorsque l’article est ouvert sur le site d’origine', - 'article_published_on' => 'Article publié initialement sur %s', - 'article_published_on_author' => 'Article publié initialement sur %s par %s', - 'article_viewed' => 'lorsque l’article est affiché', - 'articles' => 'articles', - 'articles_per_page' => 'Nombre d’articles par page', - 'articles_to_display' => 'Articles à afficher', - 'ask_empty' => 'Vider ?', - 'attention' => 'Attention !', - 'aug' => 'août', - 'august' => 'août', - 'auth_form' => 'Formulaire (traditionnel, requiert JavaScript)', - 'auth_form_not_set' => 'Un problème est survenu lors de la configuration de votre système d’authentification. Veuillez réessayer plus tard.', - 'auth_form_set' => 'Le formulaire est désormais votre système d’authentification.', - 'auth_no_password_set' => 'Aucun mot de passe administrateur n’a été précisé. Cette fonctionnalité n’est pas disponible.', - 'auth_none' => 'Aucune (dangereux)', - 'auth_not_persona' => 'Seul le système d’authentification Persona peut être réinitialisé.', - 'auth_persona' => 'Mozilla Persona (moderne, requiert JavaScript)', - 'auth_reset' => 'Réinitialisation de l’authentification', - 'auth_token' => 'Jeton d’identification', - 'auth_type' => 'Méthode d’authentification', - 'auth_will_reset' => 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.', - 'author' => 'Auteur', - 'auto_load_more' => 'Charger les articles suivants en bas de page', - 'auto_read_when' => 'Marquer un article comme lu…', - 'auto_share' => 'Partager', - 'auto_share_help' => 'S’il n’y a qu’un mode de partage, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', - 'back_to_rss_feeds' => '← Retour à vos flux RSS', - 'bad_opml_file' => 'Votre fichier OPML n’est pas valide.', - 'base_url' => 'Base de l’URL', - 'bdd' => 'Base de données', - 'bdd_conf_is_ko' => 'Vérifiez les informations d’accès à la base de données.', - 'bdd_conf_is_ok' => 'La configuration de la base de données a été enregistrée.', - 'bdd_configuration' => 'Base de données', - 'bdd_type' => 'Type de base de données', - 'before_one_day' => 'Antérieurs à 1 jour', - 'before_one_week' => 'Antérieurs à 1 semaine', - 'before_yesterday' => 'À partir d’avant-hier', - 'blank_to_disable' => 'Laissez vide pour désactiver', - 'blogotext' => 'Blogotext', - 'bookmark' => 'S’abonner (bookmark FreshRSS)', - 'bottom_line' => 'Ligne du bas', - 'bugs_reports' => 'Rapports de bugs', - 'by' => 'par', - 'by_author' => 'Par %s', - 'by_default' => 'Par défaut', - 'by_email' => 'Par courriel', - 'by_feed' => 'par flux', - 'cache_is_ok' => 'Les droits sur le répertoire de cache sont bons', - 'can_not_be_deleted' => 'Ne peut pas être supprimée.', - 'cancel' => 'Annuler', - 'categories' => 'Catégories', - 'categories_management' => 'Gestion des catégories', - 'categories_updated' => 'Les catégories ont été mises à jour.', - 'categorize' => 'Ranger dans une catégorie', - 'category' => 'Catégorie', - 'category_created' => 'La catégorie %s a été créée.', - 'category_deleted' => 'La catégorie a été supprimée.', - 'category_emptied' => 'La catégorie a été vidée.', - 'category_empty' => 'Catégorie vide', - 'category_name_exists' => 'Une catégorie possède déjà ce nom.', - 'category_no_id' => 'Vous devez préciser l’id de la catégorie.', - 'category_no_name' => 'Vous devez préciser un nom pour la catégorie.', - 'category_not_delete_default' => 'Vous ne pouvez pas supprimer la catégorie par défaut !', - 'category_not_exist' => 'Cette catégorie n’existe pas !', - 'category_number' => 'Catégorie n°%d', - 'category_updated' => 'La catégorie a été mise à jour.', - 'change_value' => 'Vous devriez changer cette valeur par n’importe quelle autre', - 'checks' => 'Vérifications', - 'choose_language' => 'Choisissez la langue pour FreshRSS', - 'clear_logs' => 'Effacer les logs', - 'collapse_article' => 'Refermer', - 'conf.users.articles_and_size' => '%s articles (%s)', - 'configuration' => 'Configuration', - 'configuration_updated' => 'La configuration a été mise à jour.', - 'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !', - 'confirm_action_feed_cat' => 'Êtes-vous sûr(e) de vouloir continuer ? Vous perdrez les favoris et les filtres associés. Cette action ne peut être annulée !', - 'congratulations' => 'Félicitations !', - 'content_width' => 'Largeur du contenu', - 'create' => 'Créer', - 'create_user' => 'Créer un nouvel utilisateur', - 'credits' => 'Crédits', - 'credits_content' => 'Des éléments de design sont issus du projet Bootstrap bien que FreshRSS n’utilise pas ce framework. Les icônes sont issues du projet GNOME. La police Open Sans utilisée a été créée par Steve Matteson. Les favicons sont récupérés grâce au site getFavicon. FreshRSS repose sur Minz, un framework PHP.', - 'css_path_on_website' => 'Sélecteur CSS des articles sur le site d’origine', - 'ctype_is_nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype)', - 'ctype_is_ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype)', - 'curl_is_nok' => 'Vous ne disposez pas de cURL (paquet php5-curl)', - 'curl_is_ok' => 'Vous disposez de cURL dans sa version %s', - 'current_user' => 'Utilisateur actuel', - 'damn' => 'Arf !', - 'data_is_ok' => 'Les droits sur le répertoire de data sont bons', - 'dec' => 'déc.', - 'december' => 'décembre', - 'default_category' => 'Sans catégorie', - 'default_user' => 'Nom de l’utilisateur par défaut (16 caractères alphanumériques maximum)', - 'default_view' => 'Vue par défaut', - 'delete' => 'Supprimer', - 'delete_articles_every' => 'Supprimer les articles après', - 'diaspora' => 'Diaspora*', - 'display' => 'Affichage', - 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', - 'display_categories_unfolded' => 'Afficher les catégories pliées par défaut', - 'display_configuration' => 'Affichage', - 'do_not_change_if_doubt' => 'Laissez tel quel dans le doute', - 'dom_is_nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml)', - 'dom_is_ok' => 'Vous disposez du nécessaire pour parcourir le DOM', - 'email' => 'Courriel', - 'error_occurred' => 'Une erreur est survenue !', - 'error_occurred_update' => 'Rien n’a été modifié !', - 'explain_token' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.
    %s?output=rss&token=%s', - 'export' => 'Exporter', - 'export_no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.', - 'export_opml' => 'Exporter la liste des flux (OPML)', - 'export_starred' => 'Exporter les favoris', - 'facebook' => 'Facebook', - 'favicons_is_ok' => 'Les droits sur le répertoire des favicons sont bons', - 'favorite_feeds' => 'Favoris (%s)', - 'feb' => 'fév.', - 'february' => 'février', - 'feed' => 'Flux', - 'feed_actualized' => '%s a été mis à jour.', - 'feed_added' => 'Le flux %s a bien été ajouté.', - 'feed_deleted' => 'Le flux a été supprimé.', - 'feed_description' => 'Description', - 'feed_empty' => 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.', - 'feed_in_error' => 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.', - 'feed_list' => 'Liste des articles de %s', - 'feed_not_added' => '%s n’a pas pu être ajouté.', - 'feed_updated' => 'Le flux a été mis à jour.', - 'feed_url' => 'URL du flux', - 'feed_validator' => 'Vérifier la valididé du flux', - 'feedback.login.error' => 'L’identifiant est invalide !', - 'feedback.login.success' => 'Vous êtes désormais connecté', - 'feedback.logout.success' => 'Vous avez été déconnecté', - 'feedback.user_profile.updated' => 'Votre profil a été mis à jour', - 'feeds' => 'Flux', - 'feeds_actualized' => 'Les flux ont été mis à jour.', - 'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.', - 'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues.', - 'feeds_marked_read' => 'Les flux ont été marqués comme lus.', - 'feeds_moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans %s.', - 'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé !', - 'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans', - 'file_to_import' => 'Fichier à importer
    (OPML, Json ou Zip)', - 'file_to_import_no_zip' => 'Fichier à importer
    (OPML ou Json)', - 'filter' => 'Filtrer', - 'finish_installation' => 'Terminer l’installation', - 'first' => 'Début', - 'first_article' => 'Passer au premier article', - 'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.', - 'focus_search' => 'Accéder à la recherche', - 'format_date' => 'j %s Y', - 'format_date_hour' => 'j %s Y \\à H\\:i', - 'freshrss' => 'FreshRSS', - 'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de Kriss Feed ou Leed. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.', - 'freshrss_installation' => 'Installation · FreshRSS', - 'fri' => 'ven.', - 'g+' => 'Google+', - 'gen.menu.admin' => 'Administration', - 'gen.menu.authentication' => 'Authentification', - 'gen.menu.check_install' => 'Vérification de l\'installation', - 'gen.menu.user_management' => 'Gestion des utilisateurs', - 'gen.menu.user_profile' => 'Profil', - 'gen.title.authentication' => 'Authentification', - 'gen.title.check_install' => 'Vérification de l\'installation', - 'gen.title.global_view' => 'Vue globale', - 'gen.title.user_management' => 'Gestion des utilisateurs', - 'gen.title.user_profile' => 'Profil', - 'general_conf_is_ok' => 'La configuration générale a été enregistrée.', - 'general_configuration' => 'Configuration générale', - 'github_or_email' => 'sur Github ou par courriel', - 'global_view' => 'Vue globale', - 'help' => 'Afficher la documentation', - 'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)', - 'host' => 'Hôte', - 'html5_notif_timeout' => 'Temps d’affichage de la notification HTML5', - 'http_auth' => 'HTTP (pour utilisateurs avancés avec HTTPS)', - 'http_authentication' => 'Authentification HTTP', - 'http_password' => 'Mot de passe HTTP', - 'http_referer_is_nok' => 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.', - 'http_referer_is_ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.', - 'http_username' => 'Identifiant HTTP', - 'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images', - 'import' => 'Importer', - 'import_export' => 'Importer / exporter', - 'informations' => 'Informations', - 'install_not_deleted' => 'Quelque chose s’est mal passé, vous devez supprimer le fichier %s à la main.', - 'installation_is_ok' => 'L’installation s’est bien passée.
    La dernière étape va maintenant tenter de supprimer les fichiers ainsi que d’éventuelles copies de base de données créés durant le processus de mise à jour.
    Vous pouvez choisir de sauter cette étape en supprimant ./data/do-install.txt manuellement.', - 'installation_step' => 'Installation — étape %d · FreshRSS', - 'internal_problem_feed' => 'Le flux ne peut pas être ajouté. Consulter les logs de FreshRSS pour plus de détails.', - 'invalid_login' => 'L’identifiant est invalide !', - 'invalid_url' => 'L’url %s est invalide.', - 'is_admin' => 'est administrateur', - 'jan' => 'jan.', - 'january' => 'janvier', - 'javascript_for_shortcuts' => 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis.', - 'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé', - 'javascript_should_be_activated' => 'Le JavaScript doit être activé.', - 'jul' => 'jui.', - 'july' => 'juillet', - 'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)', - 'jun' => 'juin', - 'june' => 'juin', - 'keep_history' => 'Nombre minimum d’articles à conserver', - 'keep_logged_in' => 'Rester connecté (1 mois)', - 'language' => 'Langue', - 'language_defined' => 'La langue a bien été définie.', - 'last' => 'Fin', - 'last_3_month' => 'Depuis les trois derniers mois', - 'last_6_month' => 'Depuis les six derniers mois', - 'last_article' => 'Passer au dernier article', - 'last_month' => 'Depuis le mois dernier', - 'last_week' => 'Depuis la semaine dernière', - 'last_year' => 'Depuis l’année dernière', - 'lead_developer' => 'Développeur principal', - 'license' => 'Licence', - 'load_more' => 'Charger plus d’articles', - 'log_is_ok' => 'Les droits sur le répertoire des logs sont bons', - 'login' => 'Connexion', - 'login_configuration' => 'Identification', - 'login_persona_problem' => 'Problème de connexion à Persona ?', - 'login_required' => 'Accès protégé par mot de passe :', - 'login_with_persona' => 'Connexion avec Persona', - 'logout' => 'Déconnexion', - 'logs' => 'Logs', - 'logs_empty' => 'Les logs sont vides.', - 'main_stream' => 'Flux principal', - 'mar' => 'mar.', - 'march' => 'mars', - 'mark_all_read' => 'Tout marquer comme lu', - 'mark_cat_read' => 'Marquer la catégorie comme lue', - 'mark_favorite' => 'Mettre en favori', - 'mark_feed_read' => 'Marquer le flux comme lu', - 'mark_read' => 'Marquer comme lu', - 'may' => 'mai.', - 'minz_is_nok' => 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script build.sh ou bien la télécharger sur Github et installer dans le répertoire %s le contenu de son répertoire /lib.', - 'minz_is_ok' => 'Vous disposez du framework Minz', - 'mon' => 'lun.', - 'month' => 'mois', - 'more_information' => 'Plus d’informations', - 'n_entries_deleted' => '%d articles ont été supprimés.', - 'n_feeds_actualized' => '%d flux ont été mis à jour.', - 'new_article' => 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.', - 'new_category' => 'Nouvelle catégorie', - 'newer_first' => 'Plus récents en premier', - 'next' => 'Suivant', - 'next_article' => 'Passer à l’article suivant', - 'next_page' => 'Passer à la page suivante', - 'next_step' => 'Passer à l’étape suivante', - 'no' => 'Non', - 'no_feed_actualized' => 'Aucun flux n’a pu être mis à jour.', - 'no_feed_to_display' => 'Il n’y a aucun article à afficher.', - 'no_feed_to_refresh' => 'Il n’y a aucun flux à actualiser…', - 'no_query' => 'Vous n’avez pas encore créé de filtre.', - 'no_query_filter' => 'Aucun filtre appliqué', - 'no_rss_feed' => 'Aucun flux RSS', - 'no_selected_feed' => 'Aucun flux sélectionné.', - 'no_update' => 'Aucune mise à jour à appliquer', - 'no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur.', - 'normal_view' => 'Vue normale', - 'not_read' => '%d non lu', - 'not_reads' => '%d non lus', - 'not_yet_implemented' => 'Pas encore implémenté', - 'nothing_to_load' => 'Fin des articles', - 'notif_body_new_articles' => 'Il y a \\d nouveaux articles à lire sur FreshRSS.', - 'notif_title_new_articles' => 'FreshRSS : nouveaux articles !', - 'nov' => 'nov.', - 'november' => 'novembre', - 'number_articles' => '%d articles', - 'number_divided_when_reader' => 'Divisé par 2 dans la vue de lecture.', - 'number_feeds' => '%d flux', - 'oct' => 'oct.', - 'october' => 'octobre', - 'ok' => 'Ok !', - 'older_first' => 'Plus anciens en premier', - 'oops' => 'Oups !', - 'optimization_complete' => 'Optimisation terminée.', - 'optimize_bdd' => 'Optimiser la base de données', - 'optimize_todo_sometimes' => 'À faire de temps en temps pour réduire la taille de la BDD', - 'or' => 'ou', - 'page_not_found' => 'La page que vous cherchez n’existe pas !', - 'password' => 'Mot de passe', - 'password_api' => 'Mot de passe API
    (ex. : pour applis mobiles)', - 'password_form' => 'Mot de passe
    (pour connexion par formulaire)', - 'pcre_is_nok' => 'Il manque une librairie pour les expressions régulières (php-pcre)', - 'pcre_is_ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE)', - 'pdo_is_nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite)', - 'pdo_is_ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite)', - 'persona_connection_email' => 'Adresse courriel de connexion
    (pour Mozilla Persona)', - 'persona_is_ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons', - 'php_is_nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s', - 'php_is_ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS', - 'prefix' => 'Préfixe des tables', - 'previous' => 'Précédent', - 'previous_article' => 'Passer à l’article précédent', - 'previous_page' => 'Passer à la page précédente', - 'print' => 'Imprimer', - 'project_website' => 'Site du projet', - 'public' => 'Public', - 'publication_date' => 'Date de publication', - 'purge_completed' => 'Purge effectuée (%d articles supprimés).', - 'purge_now' => 'Purger maintenant', - 'queries' => 'Filtres utilisateurs', - 'query_created' => 'Le filtre "%s" a bien été créé.', - 'query_deprecated' => 'Ce filtre n’est plus valide. La catégorie ou le flux concerné a été supprimé.', - 'query_filter' => 'Filtres appliqués :', - 'query_get_all' => 'Afficher tous les articles', - 'query_get_category' => 'Afficher la catégorie "%s"', - 'query_get_favorite' => 'Afficher les articles favoris', - 'query_get_feed' => 'Afficher le flux "%s"', - 'query_number' => 'Filtre n°%d', - 'query_order_asc' => 'Afficher les articles les plus anciens en premier', - 'query_order_desc' => 'Afficher les articles les plus récents en premier', - 'query_search' => 'Recherche de "%s"', - 'query_state_0' => 'Afficher tous les articles', - 'query_state_1' => 'Afficher les articles lus', - 'query_state_2' => 'Afficher les articles non lus', - 'query_state_3' => 'Afficher tous les articles', - 'query_state_4' => 'Afficher les articles favoris', - 'query_state_5' => 'Afficher les articles lus et favoris', - 'query_state_6' => 'Afficher les articles non lus et favoris', - 'query_state_7' => 'Afficher les articles favoris', - 'query_state_8' => 'Afficher les articles non favoris', - 'query_state_9' => 'Afficher les articles lus et non favoris', - 'query_state_10' => 'Afficher les articles non lus et non favoris', - 'query_state_11' => 'Afficher les articles non favoris', - 'query_state_12' => 'Afficher tous les articles', - 'query_state_13' => 'Afficher les articles lus', - 'query_state_14' => 'Afficher les articles non lus', - 'query_state_15' => 'Afficher tous les articles', - 'random_string' => 'Chaîne aléatoire', - 'reader_view' => 'Vue lecture', - 'reading_configuration' => 'Lecture', - 'reading_confirm' => 'Afficher une confirmation lors des actions “marquer tout comme lu”', - 'refresh' => 'Actualisation', - 'related_tags' => 'Tags associés', - 'retrieve_truncated_feeds' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)', - 'rss_feed_management' => 'Gestion des flux RSS', - 'rss_feeds_of' => 'Flux RSS de %s', - 'rss_view' => 'Flux RSS', - 'sat' => 'sam.', - 'save' => 'Enregistrer', - 'scroll' => 'au défilement de la page', - 'search' => 'Rechercher des mots ou des #tags', - 'search_short' => 'Rechercher', - 'seconds_(0_means_no_timeout)' => 'secondes (0 signifie aucun timeout ) ', - 'see_on_website' => 'Voir sur le site d’origine', - 'see_website' => 'Voir le site', - 'sep' => 'sep.', - 'september' => 'septembre', - 'shaarli' => 'Shaarli', - 'share' => 'Partager', - 'share_name' => 'Nom du partage à afficher', - 'share_url' => 'URL du partage à utiliser', - 'sharing' => 'Partage', - 'sharing_management' => 'Gestion des options de partage', - 'shift_for_all_read' => '+ shift pour marquer tous les articles comme lus', - 'shortcuts' => 'Raccourcis', - 'shortcuts_article_action' => 'Actions associées à l’article courant', - 'shortcuts_navigation' => 'Navigation', - 'shortcuts_navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.
    Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.', - 'shortcuts_other_action' => 'Autres actions', - 'shortcuts_updated' => 'Les raccourcis ont été mis à jour.', - 'show_adaptive' => 'Adapter l’affichage', - 'show_all_articles' => 'Afficher tous les articles', - 'show_favorite' => 'Afficher les favoris', - 'show_in_all_flux' => 'Afficher dans le flux principal', - 'show_not_favorite' => 'Afficher tout sauf les favoris', - 'show_not_reads' => 'Afficher les non lus', - 'show_read' => 'Afficher les lus', - 'sort_order' => 'Ordre de tri', - 'starred_list' => 'Liste des articles favoris', - 'stats' => 'Statistiques', - 'stats_entry_count' => 'Nombre d’articles', - 'stats_entry_per_category' => 'Articles par catégorie', - 'stats_entry_per_day' => 'Nombre d’articles par jour (30 derniers jours)', - 'stats_entry_per_day_of_week' => 'Par jour de la semaine (moyenne : %.2f messages)', - 'stats_entry_per_hour' => 'Par heure (moyenne : %.2f messages)', - 'stats_entry_per_month' => 'Par mois (moyenne : %.2f messages)', - 'stats_entry_repartition' => 'Répartition des articles', - 'stats_feed_per_category' => 'Flux par catégorie', - 'stats_idle' => 'Flux inactifs', - 'stats_main' => 'Statistiques principales', - 'stats_no_idle' => 'Il n’y a aucun flux inactif !', - 'stats_percent_of_total' => '%% du total', - 'stats_repartition' => 'Répartition des articles', - 'stats_top_feed' => 'Les dix plus gros flux', - 'status_favorites' => 'favoris', - 'status_read' => 'lus', - 'status_total' => 'total', - 'status_unread' => 'non lus', - 'steps' => 'Étapes', - 'sticky_post' => 'Aligner l’article en haut quand il est ouvert', - 'sub.categories.over_max' => 'Vous avez atteint votre limite de catégories (%d)', - 'sub.feeds.over_max' => 'Vous avez atteint votre limite de flux (%d)', - 'submit' => 'Valider', - 'subscription_management' => 'Gestion des abonnements', - 'sun' => 'dim.', - 'theme' => 'Thème', - 'think_to_add' => 'Vous pouvez ajouter des flux.', - 'this_is_the_end' => 'This is the end', - 'thu' => 'jeu.', - 'title' => 'Titre', - 'today' => 'Aujourd’hui', - 'top_line' => 'Ligne du haut', - 'truncate' => 'Supprimer tous les articles', - 'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que', - 'tue' => 'mar.', - 'twitter' => 'Twitter', - 'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ', - 'update' => 'Mise à jour', - 'update_apply' => 'Appliquer la mise à jour', - 'update_can_apply' => 'Une mise à jour est disponible.', - 'update_check' => 'Vérifier les mises à jour', - 'update_end' => 'La mise à jour est terminée, vous pouvez maintenant passer à l’étape finale.', - 'update_finished' => 'La mise à jour est terminée !', - 'update_last' => 'Dernière vérification : %s', - 'update_long' => 'Ce processus peut prendre longtemps, selon la taille de votre base de données. Vous aurez peut-être à attendre que cette page dépasse son temps maximum d’exécution (~5 minutes) puis à la recharger.', - 'update_problem' => 'La mise à jour a rencontré un problème : %s', - 'update_server_not_found' => 'Le serveur de mise à jour n’a pas été trouvé. [%s]', - 'update_start' => 'Lancer la mise à jour', - 'update_system' => 'Système de mise à jour', - 'updated' => 'Modifications enregistrées.', - 'upon_reception' => 'dès la réception du nouvel article', - 'user_created' => 'L’utilisateur %s a été créé.', - 'user_deleted' => 'L’utilisateur %s a été supprimé.', - 'user_filter' => 'Accéder aux filtres utilisateur', - 'user_filter_help' => 'S’il n’y a qu’un filtre utilisateur, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', - 'username' => 'Nom d’utilisateur', - 'username_admin' => 'Nom d’utilisateur administrateur', - 'users' => 'Utilisateurs', - 'users_list' => 'Liste des utilisateurs', - 'version' => 'Version', - 'version_update' => 'Mise à jour', - 'wallabag' => 'wallabag', - 'website' => 'Site Internet', - 'website_url' => 'URL du site', - 'wed' => 'mer.', - 'width_large' => 'Large', - 'width_medium' => 'Moyenne', - 'width_no_limit' => 'Pas de limite', - 'width_thin' => 'Fine', - 'yes' => 'Oui', - 'yesterday' => 'Hier', - 'your_diaspora_pod' => 'Votre pod Diaspora*', - 'your_favorites' => 'Vos favoris', - 'your_rss_feeds' => 'Vos flux RSS', - 'your_shaarli' => 'Votre Shaarli', - 'your_wallabag' => 'Votre wallabag', - 'zip_error' => 'Une erreur est survenue durant l’import du fichier Zip.', -); \ No newline at end of file diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php new file mode 100644 index 000000000..ad1fae6c0 --- /dev/null +++ b/app/i18n/fr/admin.php @@ -0,0 +1,92 @@ + array( + 'cache' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/cache. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de cache sont bons.', + ), + 'categories' => array( + 'nok' => 'La table category est mal configurée.', + 'ok' => 'La table category est bien configurée.', + ), + 'connection' => array( + 'nok' => 'La connexion à la base de données est impossible.', + 'ok' => 'La connexion à la base de données est bonne.', + ), + 'ctype' => array( + 'nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).', + 'ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).', + ), + 'curl' => array( + 'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).', + 'ok' => 'Vous disposez de cURL.', + ), + 'data' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de data sont bons.', + ), + 'database' => 'Installation de la base de données', + 'dom' => array( + 'nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml).', + 'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.', + ), + 'entries' => array( + 'nok' => 'La table entry est mal configurée.', + 'ok' => 'La table entry est bien configurée.', + ), + 'favicons' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/favicons. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire des favicons sont bons.', + ), + 'feeds' => array( + 'nok' => 'La table feed est mal configurée.', + 'ok' => 'La table feed est bien configurée.', + ), + 'files' => 'Installation des fichiers', + 'json' => array( + 'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).', + 'ok' => 'Vous disposez de l\'extension JSON.', + ), + 'logs' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/logs. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire des logs sont bons.', + ), + 'minz' => array( + 'nok' => 'Vous ne disposez pas de la librairie Minz.', + 'ok' => 'Vous disposez du framework Minz', + ), + 'pcre' => array( + 'nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).', + 'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).', + ), + 'pdo' => array( + 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).', + 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/persona. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons.', + ), + 'php' => array( + '_' => 'Installation de PHP', + 'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.', + 'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.', + ), + 'tables' => array( + 'nok' => 'Il manque une ou plusieurs tables en base de données.', + 'ok' => 'Les tables sont bien présentes en base de données.', + ), + 'tokens' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/tokens. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire des tokens sont bons.', + ), + 'zip' => array( + 'nok' => 'Vous ne disposez pas de l\'extension ZIP (paquet php5-zip).', + 'ok' => 'Vous disposez de l\'extension ZIP.', + ), + ), + 'users' => array( + 'articles_and_size' => '%s articles (%s)', + ), +); diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php new file mode 100644 index 000000000..460804774 --- /dev/null +++ b/app/i18n/fr/conf.php @@ -0,0 +1,7 @@ + array( + 'articles_and_size' => '%s articles (%s)', + ), +); diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php new file mode 100644 index 000000000..f4bb7cccf --- /dev/null +++ b/app/i18n/fr/feedback.php @@ -0,0 +1,14 @@ + array( + 'error' => 'L’identifiant est invalide !', + 'success' => 'Vous êtes désormais connecté', + ), + 'logout' => array( + 'success' => 'Vous avez été déconnecté', + ), + 'user_profile' => array( + 'updated' => 'Votre profil a été mis à jour', + ), +); diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php new file mode 100644 index 000000000..8a4a9750b --- /dev/null +++ b/app/i18n/fr/gen.php @@ -0,0 +1,487 @@ + '\\a\\v\\r\\i\\l', + 'Aug' => '\\a\\o\\û\\t', + 'Dec' => '\\d\\é\\c\\e\\m\\b\\r\\e', + 'Feb' => '\\f\\é\\v\\r\\i\\e\\r', + 'Jan' => '\\j\\a\\n\\v\\i\\e\\r', + 'Jul' => '\\j\\u\\i\\l\\l\\e\\t', + 'Jun' => '\\j\\u\\i\\n', + 'Mar' => '\\m\\a\\r\\s', + 'May' => '\\m\\a\\i', + 'Nov' => '\\n\\o\\v\\e\\m\\b\\r\\e', + 'Oct' => '\\o\\c\\t\\o\\b\\r\\e', + 'Sep' => '\\s\\e\\p\\t\\e\\m\\b\\r\\e', + 'about' => 'À propos', + 'about_freshrss' => 'À propos de FreshRSS', + 'access_denied' => 'Vous n’avez pas le droit d’accéder à cette page !', + 'access_protected_feeds' => 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.', + 'activate_sharing' => 'Activer le partage', + 'actualize' => 'Actualiser', + 'add_category' => 'Ajouter une catégorie', + 'add_query' => 'Créer un filtre', + 'add_rss_feed' => 'Ajouter un flux RSS', + 'administration' => 'Gérer', + 'advanced' => 'Avancé', + 'after_onread' => 'Après “marquer tout comme lu”,', + 'agpl3' => 'AGPL 3', + 'all_feeds' => 'Tous les flux', + 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', + 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', + 'already_subscribed' => 'Vous êtes déjà abonné à %s', + 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applis mobiles)', + 'apr' => 'avr.', + 'april' => 'avril', + 'archiving_configuration' => 'Archivage', + 'archiving_configuration_help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.', + 'article' => 'Article', + 'article_icons' => 'Icônes d’article', + 'article_open_on_website' => 'lorsque l’article est ouvert sur le site d’origine', + 'article_published_on' => 'Article publié initialement sur %s', + 'article_published_on_author' => 'Article publié initialement sur %s par %s', + 'article_viewed' => 'lorsque l’article est affiché', + 'articles' => 'articles', + 'articles_per_page' => 'Nombre d’articles par page', + 'articles_to_display' => 'Articles à afficher', + 'ask_empty' => 'Vider ?', + 'attention' => 'Attention !', + 'aug' => 'août', + 'august' => 'août', + 'auth_form' => 'Formulaire (traditionnel, requiert JavaScript)', + 'auth_form_not_set' => 'Un problème est survenu lors de la configuration de votre système d’authentification. Veuillez réessayer plus tard.', + 'auth_form_set' => 'Le formulaire est désormais votre système d’authentification.', + 'auth_no_password_set' => 'Aucun mot de passe administrateur n’a été précisé. Cette fonctionnalité n’est pas disponible.', + 'auth_none' => 'Aucune (dangereux)', + 'auth_not_persona' => 'Seul le système d’authentification Persona peut être réinitialisé.', + 'auth_persona' => 'Mozilla Persona (moderne, requiert JavaScript)', + 'auth_reset' => 'Réinitialisation de l’authentification', + 'auth_token' => 'Jeton d’identification', + 'auth_type' => 'Méthode d’authentification', + 'auth_will_reset' => 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.', + 'author' => 'Auteur', + 'auto_load_more' => 'Charger les articles suivants en bas de page', + 'auto_read_when' => 'Marquer un article comme lu…', + 'auto_share' => 'Partager', + 'auto_share_help' => 'S’il n’y a qu’un mode de partage, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', + 'back_to_rss_feeds' => '← Retour à vos flux RSS', + 'bad_opml_file' => 'Votre fichier OPML n’est pas valide.', + 'base_url' => 'Base de l’URL', + 'bdd' => 'Base de données', + 'bdd_conf_is_ko' => 'Vérifiez les informations d’accès à la base de données.', + 'bdd_conf_is_ok' => 'La configuration de la base de données a été enregistrée.', + 'bdd_configuration' => 'Base de données', + 'bdd_type' => 'Type de base de données', + 'before_one_day' => 'Antérieurs à 1 jour', + 'before_one_week' => 'Antérieurs à 1 semaine', + 'before_yesterday' => 'À partir d’avant-hier', + 'blank_to_disable' => 'Laissez vide pour désactiver', + 'blogotext' => 'Blogotext', + 'bookmark' => 'S’abonner (bookmark FreshRSS)', + 'bottom_line' => 'Ligne du bas', + 'bugs_reports' => 'Rapports de bugs', + 'by' => 'par', + 'by_author' => 'Par %s', + 'by_default' => 'Par défaut', + 'by_email' => 'Par courriel', + 'by_feed' => 'par flux', + 'cache_is_ok' => 'Les droits sur le répertoire de cache sont bons', + 'can_not_be_deleted' => 'Ne peut pas être supprimée.', + 'cancel' => 'Annuler', + 'categories' => 'Catégories', + 'categories_management' => 'Gestion des catégories', + 'categories_updated' => 'Les catégories ont été mises à jour.', + 'categorize' => 'Ranger dans une catégorie', + 'category' => 'Catégorie', + 'category_created' => 'La catégorie %s a été créée.', + 'category_deleted' => 'La catégorie a été supprimée.', + 'category_emptied' => 'La catégorie a été vidée.', + 'category_empty' => 'Catégorie vide', + 'category_name_exists' => 'Une catégorie possède déjà ce nom.', + 'category_no_id' => 'Vous devez préciser l’id de la catégorie.', + 'category_no_name' => 'Vous devez préciser un nom pour la catégorie.', + 'category_not_delete_default' => 'Vous ne pouvez pas supprimer la catégorie par défaut !', + 'category_not_exist' => 'Cette catégorie n’existe pas !', + 'category_number' => 'Catégorie n°%d', + 'category_updated' => 'La catégorie a été mise à jour.', + 'change_value' => 'Vous devriez changer cette valeur par n’importe quelle autre', + 'checks' => 'Vérifications', + 'choose_language' => 'Choisissez la langue pour FreshRSS', + 'clear_logs' => 'Effacer les logs', + 'collapse_article' => 'Refermer', + 'configuration' => 'Configuration', + 'configuration_updated' => 'La configuration a été mise à jour.', + 'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !', + 'confirm_action_feed_cat' => 'Êtes-vous sûr(e) de vouloir continuer ? Vous perdrez les favoris et les filtres associés. Cette action ne peut être annulée !', + 'congratulations' => 'Félicitations !', + 'content_width' => 'Largeur du contenu', + 'create' => 'Créer', + 'create_user' => 'Créer un nouvel utilisateur', + 'credits' => 'Crédits', + 'credits_content' => 'Des éléments de design sont issus du projet Bootstrap bien que FreshRSS n’utilise pas ce framework. Les icônes sont issues du projet GNOME. La police Open Sans utilisée a été créée par Steve Matteson. Les favicons sont récupérés grâce au site getFavicon. FreshRSS repose sur Minz, un framework PHP.', + 'css_path_on_website' => 'Sélecteur CSS des articles sur le site d’origine', + 'ctype_is_nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype)', + 'ctype_is_ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype)', + 'curl_is_nok' => 'Vous ne disposez pas de cURL (paquet php5-curl)', + 'curl_is_ok' => 'Vous disposez de cURL dans sa version %s', + 'current_user' => 'Utilisateur actuel', + 'damn' => 'Arf !', + 'data_is_ok' => 'Les droits sur le répertoire de data sont bons', + 'dec' => 'déc.', + 'december' => 'décembre', + 'default_category' => 'Sans catégorie', + 'default_user' => 'Nom de l’utilisateur par défaut (16 caractères alphanumériques maximum)', + 'default_view' => 'Vue par défaut', + 'delete' => 'Supprimer', + 'delete_articles_every' => 'Supprimer les articles après', + 'diaspora' => 'Diaspora*', + 'display' => 'Affichage', + 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', + 'display_categories_unfolded' => 'Afficher les catégories pliées par défaut', + 'display_configuration' => 'Affichage', + 'do_not_change_if_doubt' => 'Laissez tel quel dans le doute', + 'dom_is_nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml)', + 'dom_is_ok' => 'Vous disposez du nécessaire pour parcourir le DOM', + 'email' => 'Courriel', + 'error_occurred' => 'Une erreur est survenue !', + 'error_occurred_update' => 'Rien n’a été modifié !', + 'explain_token' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.
    %s?output=rss&token=%s', + 'export' => 'Exporter', + 'export_no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.', + 'export_opml' => 'Exporter la liste des flux (OPML)', + 'export_starred' => 'Exporter les favoris', + 'facebook' => 'Facebook', + 'favicons_is_ok' => 'Les droits sur le répertoire des favicons sont bons', + 'favorite_feeds' => 'Favoris (%s)', + 'feb' => 'fév.', + 'february' => 'février', + 'feed' => 'Flux', + 'feed_actualized' => '%s a été mis à jour.', + 'feed_added' => 'Le flux %s a bien été ajouté.', + 'feed_deleted' => 'Le flux a été supprimé.', + 'feed_description' => 'Description', + 'feed_empty' => 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.', + 'feed_in_error' => 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.', + 'feed_list' => 'Liste des articles de %s', + 'feed_not_added' => '%s n’a pas pu être ajouté.', + 'feed_updated' => 'Le flux a été mis à jour.', + 'feed_url' => 'URL du flux', + 'feed_validator' => 'Vérifier la valididé du flux', + 'feeds' => 'Flux', + 'feeds_actualized' => 'Les flux ont été mis à jour.', + 'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.', + 'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues.', + 'feeds_marked_read' => 'Les flux ont été marqués comme lus.', + 'feeds_moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans %s.', + 'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé !', + 'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans', + 'file_to_import' => 'Fichier à importer
    (OPML, Json ou Zip)', + 'file_to_import_no_zip' => 'Fichier à importer
    (OPML ou Json)', + 'filter' => 'Filtrer', + 'finish_installation' => 'Terminer l’installation', + 'first' => 'Début', + 'first_article' => 'Passer au premier article', + 'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.', + 'focus_search' => 'Accéder à la recherche', + 'format_date' => 'j %s Y', + 'format_date_hour' => 'j %s Y \\à H\\:i', + 'freshrss' => 'FreshRSS', + 'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de Kriss Feed ou Leed. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.', + 'freshrss_installation' => 'Installation · FreshRSS', + 'fri' => 'ven.', + 'g+' => 'Google+', + 'menu' => array( + 'admin' => 'Administration', + 'authentication' => 'Authentification', + 'check_install' => 'Vérification de l’installation', + 'user_management' => 'Gestion des utilisateurs', + 'user_profile' => 'Profil', + ), + 'title' => array( + '_' => 'Titre', + 'authentication' => 'Authentification', + 'check_install' => 'Vérification de l’installation', + 'global_view' => 'Vue globale', + 'user_management' => 'Gestion des utilisateurs', + 'user_profile' => 'Profil', + ), + 'general_conf_is_ok' => 'La configuration générale a été enregistrée.', + 'general_configuration' => 'Configuration générale', + 'github_or_email' => 'sur Github ou par courriel', + 'global_view' => 'Vue globale', + 'help' => 'Afficher la documentation', + 'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)', + 'host' => 'Hôte', + 'html5_notif_timeout' => 'Temps d’affichage de la notification HTML5', + 'http_auth' => 'HTTP (pour utilisateurs avancés avec HTTPS)', + 'http_authentication' => 'Authentification HTTP', + 'http_password' => 'Mot de passe HTTP', + 'http_referer_is_nok' => 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.', + 'http_referer_is_ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.', + 'http_username' => 'Identifiant HTTP', + 'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images', + 'import' => 'Importer', + 'import_export' => 'Importer / exporter', + 'informations' => 'Informations', + 'install_not_deleted' => 'Quelque chose s’est mal passé, vous devez supprimer le fichier %s à la main.', + 'installation_is_ok' => 'L’installation s’est bien passée.
    La dernière étape va maintenant tenter de supprimer les fichiers ainsi que d’éventuelles copies de base de données créés durant le processus de mise à jour.
    Vous pouvez choisir de sauter cette étape en supprimant ./data/do-install.txt manuellement.', + 'installation_step' => 'Installation — étape %d · FreshRSS', + 'internal_problem_feed' => 'Le flux ne peut pas être ajouté. Consulter les logs de FreshRSS pour plus de détails.', + 'invalid_login' => 'L’identifiant est invalide !', + 'invalid_url' => 'L’url %s est invalide.', + 'is_admin' => 'est administrateur', + 'jan' => 'jan.', + 'january' => 'janvier', + 'javascript_for_shortcuts' => 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis.', + 'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé', + 'javascript_should_be_activated' => 'Le JavaScript doit être activé.', + 'jul' => 'jui.', + 'july' => 'juillet', + 'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)', + 'jun' => 'juin', + 'june' => 'juin', + 'keep_history' => 'Nombre minimum d’articles à conserver', + 'keep_logged_in' => 'Rester connecté (1 mois)', + 'language' => 'Langue', + 'language_defined' => 'La langue a bien été définie.', + 'last' => 'Fin', + 'last_3_month' => 'Depuis les trois derniers mois', + 'last_6_month' => 'Depuis les six derniers mois', + 'last_article' => 'Passer au dernier article', + 'last_month' => 'Depuis le mois dernier', + 'last_week' => 'Depuis la semaine dernière', + 'last_year' => 'Depuis l’année dernière', + 'lead_developer' => 'Développeur principal', + 'license' => 'Licence', + 'load_more' => 'Charger plus d’articles', + 'log_is_ok' => 'Les droits sur le répertoire des logs sont bons', + 'login' => 'Connexion', + 'login_configuration' => 'Identification', + 'login_persona_problem' => 'Problème de connexion à Persona ?', + 'login_required' => 'Accès protégé par mot de passe :', + 'login_with_persona' => 'Connexion avec Persona', + 'logout' => 'Déconnexion', + 'logs' => 'Logs', + 'logs_empty' => 'Les logs sont vides.', + 'main_stream' => 'Flux principal', + 'mar' => 'mar.', + 'march' => 'mars', + 'mark_all_read' => 'Tout marquer comme lu', + 'mark_cat_read' => 'Marquer la catégorie comme lue', + 'mark_favorite' => 'Mettre en favori', + 'mark_feed_read' => 'Marquer le flux comme lu', + 'mark_read' => 'Marquer comme lu', + 'may' => 'mai.', + 'minz_is_nok' => 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script build.sh ou bien la télécharger sur Github et installer dans le répertoire %s le contenu de son répertoire /lib.', + 'minz_is_ok' => 'Vous disposez du framework Minz', + 'mon' => 'lun.', + 'month' => 'mois', + 'more_information' => 'Plus d’informations', + 'n_entries_deleted' => '%d articles ont été supprimés.', + 'n_feeds_actualized' => '%d flux ont été mis à jour.', + 'new_article' => 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.', + 'new_category' => 'Nouvelle catégorie', + 'newer_first' => 'Plus récents en premier', + 'next' => 'Suivant', + 'next_article' => 'Passer à l’article suivant', + 'next_page' => 'Passer à la page suivante', + 'next_step' => 'Passer à l’étape suivante', + 'no' => 'Non', + 'no_feed_actualized' => 'Aucun flux n’a pu être mis à jour.', + 'no_feed_to_display' => 'Il n’y a aucun article à afficher.', + 'no_feed_to_refresh' => 'Il n’y a aucun flux à actualiser…', + 'no_query' => 'Vous n’avez pas encore créé de filtre.', + 'no_query_filter' => 'Aucun filtre appliqué', + 'no_rss_feed' => 'Aucun flux RSS', + 'no_selected_feed' => 'Aucun flux sélectionné.', + 'no_update' => 'Aucune mise à jour à appliquer', + 'no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur.', + 'normal_view' => 'Vue normale', + 'not_read' => '%d non lu', + 'not_reads' => '%d non lus', + 'not_yet_implemented' => 'Pas encore implémenté', + 'nothing_to_load' => 'Fin des articles', + 'notif_body_new_articles' => 'Il y a \\d nouveaux articles à lire sur FreshRSS.', + 'notif_title_new_articles' => 'FreshRSS : nouveaux articles !', + 'nov' => 'nov.', + 'november' => 'novembre', + 'number_articles' => '%d articles', + 'number_divided_when_reader' => 'Divisé par 2 dans la vue de lecture.', + 'number_feeds' => '%d flux', + 'oct' => 'oct.', + 'october' => 'octobre', + 'ok' => 'Ok !', + 'older_first' => 'Plus anciens en premier', + 'oops' => 'Oups !', + 'optimization_complete' => 'Optimisation terminée.', + 'optimize_bdd' => 'Optimiser la base de données', + 'optimize_todo_sometimes' => 'À faire de temps en temps pour réduire la taille de la BDD', + 'or' => 'ou', + 'page_not_found' => 'La page que vous cherchez n’existe pas !', + 'password' => 'Mot de passe', + 'password_api' => 'Mot de passe API
    (ex. : pour applis mobiles)', + 'password_form' => 'Mot de passe
    (pour connexion par formulaire)', + 'pcre_is_nok' => 'Il manque une librairie pour les expressions régulières (php-pcre)', + 'pcre_is_ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE)', + 'pdo_is_nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite)', + 'pdo_is_ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite)', + 'persona_connection_email' => 'Adresse courriel de connexion
    (pour Mozilla Persona)', + 'persona_is_ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons', + 'php_is_nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s', + 'php_is_ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS', + 'prefix' => 'Préfixe des tables', + 'previous' => 'Précédent', + 'previous_article' => 'Passer à l’article précédent', + 'previous_page' => 'Passer à la page précédente', + 'print' => 'Imprimer', + 'project_website' => 'Site du projet', + 'public' => 'Public', + 'publication_date' => 'Date de publication', + 'purge_completed' => 'Purge effectuée (%d articles supprimés).', + 'purge_now' => 'Purger maintenant', + 'queries' => 'Filtres utilisateurs', + 'query_created' => 'Le filtre "%s" a bien été créé.', + 'query_deprecated' => 'Ce filtre n’est plus valide. La catégorie ou le flux concerné a été supprimé.', + 'query_filter' => 'Filtres appliqués :', + 'query_get_all' => 'Afficher tous les articles', + 'query_get_category' => 'Afficher la catégorie "%s"', + 'query_get_favorite' => 'Afficher les articles favoris', + 'query_get_feed' => 'Afficher le flux "%s"', + 'query_number' => 'Filtre n°%d', + 'query_order_asc' => 'Afficher les articles les plus anciens en premier', + 'query_order_desc' => 'Afficher les articles les plus récents en premier', + 'query_search' => 'Recherche de "%s"', + 'query_state_0' => 'Afficher tous les articles', + 'query_state_1' => 'Afficher les articles lus', + 'query_state_2' => 'Afficher les articles non lus', + 'query_state_3' => 'Afficher tous les articles', + 'query_state_4' => 'Afficher les articles favoris', + 'query_state_5' => 'Afficher les articles lus et favoris', + 'query_state_6' => 'Afficher les articles non lus et favoris', + 'query_state_7' => 'Afficher les articles favoris', + 'query_state_8' => 'Afficher les articles non favoris', + 'query_state_9' => 'Afficher les articles lus et non favoris', + 'query_state_10' => 'Afficher les articles non lus et non favoris', + 'query_state_11' => 'Afficher les articles non favoris', + 'query_state_12' => 'Afficher tous les articles', + 'query_state_13' => 'Afficher les articles lus', + 'query_state_14' => 'Afficher les articles non lus', + 'query_state_15' => 'Afficher tous les articles', + 'random_string' => 'Chaîne aléatoire', + 'reader_view' => 'Vue lecture', + 'reading_configuration' => 'Lecture', + 'reading_confirm' => 'Afficher une confirmation lors des actions “marquer tout comme lu”', + 'refresh' => 'Actualisation', + 'related_tags' => 'Tags associés', + 'retrieve_truncated_feeds' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)', + 'rss_feed_management' => 'Gestion des flux RSS', + 'rss_feeds_of' => 'Flux RSS de %s', + 'rss_view' => 'Flux RSS', + 'sat' => 'sam.', + 'save' => 'Enregistrer', + 'scroll' => 'au défilement de la page', + 'search' => 'Rechercher des mots ou des #tags', + 'search_short' => 'Rechercher', + 'seconds_(0_means_no_timeout)' => 'secondes (0 signifie aucun timeout ) ', + 'see_on_website' => 'Voir sur le site d’origine', + 'see_website' => 'Voir le site', + 'sep' => 'sep.', + 'september' => 'septembre', + 'shaarli' => 'Shaarli', + 'share' => 'Partager', + 'share_name' => 'Nom du partage à afficher', + 'share_url' => 'URL du partage à utiliser', + 'sharing' => 'Partage', + 'sharing_management' => 'Gestion des options de partage', + 'shift_for_all_read' => '+ shift pour marquer tous les articles comme lus', + 'shortcuts' => 'Raccourcis', + 'shortcuts_article_action' => 'Actions associées à l’article courant', + 'shortcuts_navigation' => 'Navigation', + 'shortcuts_navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.
    Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.', + 'shortcuts_other_action' => 'Autres actions', + 'shortcuts_updated' => 'Les raccourcis ont été mis à jour.', + 'show_adaptive' => 'Adapter l’affichage', + 'show_all_articles' => 'Afficher tous les articles', + 'show_favorite' => 'Afficher les favoris', + 'show_in_all_flux' => 'Afficher dans le flux principal', + 'show_not_favorite' => 'Afficher tout sauf les favoris', + 'show_not_reads' => 'Afficher les non lus', + 'show_read' => 'Afficher les lus', + 'sort_order' => 'Ordre de tri', + 'starred_list' => 'Liste des articles favoris', + 'stats' => 'Statistiques', + 'stats_entry_count' => 'Nombre d’articles', + 'stats_entry_per_category' => 'Articles par catégorie', + 'stats_entry_per_day' => 'Nombre d’articles par jour (30 derniers jours)', + 'stats_entry_per_day_of_week' => 'Par jour de la semaine (moyenne : %.2f messages)', + 'stats_entry_per_hour' => 'Par heure (moyenne : %.2f messages)', + 'stats_entry_per_month' => 'Par mois (moyenne : %.2f messages)', + 'stats_entry_repartition' => 'Répartition des articles', + 'stats_feed_per_category' => 'Flux par catégorie', + 'stats_idle' => 'Flux inactifs', + 'stats_main' => 'Statistiques principales', + 'stats_no_idle' => 'Il n’y a aucun flux inactif !', + 'stats_percent_of_total' => '%% du total', + 'stats_repartition' => 'Répartition des articles', + 'stats_top_feed' => 'Les dix plus gros flux', + 'status_favorites' => 'favoris', + 'status_read' => 'lus', + 'status_total' => 'total', + 'status_unread' => 'non lus', + 'steps' => 'Étapes', + 'sticky_post' => 'Aligner l’article en haut quand il est ouvert', + 'submit' => 'Valider', + 'subscription_management' => 'Gestion des abonnements', + 'sun' => 'dim.', + 'theme' => 'Thème', + 'think_to_add' => 'Vous pouvez ajouter des flux.', + 'this_is_the_end' => 'This is the end', + 'thu' => 'jeu.', + 'today' => 'Aujourd’hui', + 'top_line' => 'Ligne du haut', + 'truncate' => 'Supprimer tous les articles', + 'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que', + 'tue' => 'mar.', + 'twitter' => 'Twitter', + 'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ', + 'update' => 'Mise à jour', + 'update_apply' => 'Appliquer la mise à jour', + 'update_can_apply' => 'Une mise à jour est disponible.', + 'update_check' => 'Vérifier les mises à jour', + 'update_end' => 'La mise à jour est terminée, vous pouvez maintenant passer à l’étape finale.', + 'update_finished' => 'La mise à jour est terminée !', + 'update_last' => 'Dernière vérification : %s', + 'update_long' => 'Ce processus peut prendre longtemps, selon la taille de votre base de données. Vous aurez peut-être à attendre que cette page dépasse son temps maximum d’exécution (~5 minutes) puis à la recharger.', + 'update_problem' => 'La mise à jour a rencontré un problème : %s', + 'update_server_not_found' => 'Le serveur de mise à jour n’a pas été trouvé. [%s]', + 'update_start' => 'Lancer la mise à jour', + 'update_system' => 'Système de mise à jour', + 'updated' => 'Modifications enregistrées.', + 'upon_reception' => 'dès la réception du nouvel article', + 'user_created' => 'L’utilisateur %s a été créé.', + 'user_deleted' => 'L’utilisateur %s a été supprimé.', + 'user_filter' => 'Accéder aux filtres utilisateur', + 'user_filter_help' => 'S’il n’y a qu’un filtre utilisateur, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', + 'username' => 'Nom d’utilisateur', + 'username_admin' => 'Nom d’utilisateur administrateur', + 'users' => 'Utilisateurs', + 'users_list' => 'Liste des utilisateurs', + 'version' => 'Version', + 'version_update' => 'Mise à jour', + 'wallabag' => 'wallabag', + 'website' => 'Site Internet', + 'website_url' => 'URL du site', + 'wed' => 'mer.', + 'width_large' => 'Large', + 'width_medium' => 'Moyenne', + 'width_no_limit' => 'Pas de limite', + 'width_thin' => 'Fine', + 'yes' => 'Oui', + 'yesterday' => 'Hier', + 'your_diaspora_pod' => 'Votre pod Diaspora*', + 'your_favorites' => 'Vos favoris', + 'your_rss_feeds' => 'Vos flux RSS', + 'your_shaarli' => 'Votre Shaarli', + 'your_wallabag' => 'Votre wallabag', + 'zip_error' => 'Une erreur est survenue durant l’import du fichier Zip.', +); diff --git a/app/i18n/fr/index.php b/app/i18n/fr/index.php new file mode 100644 index 000000000..afca37ed3 --- /dev/null +++ b/app/i18n/fr/index.php @@ -0,0 +1,5 @@ + array( + 'over_max' => 'Vous avez atteint votre limite de catégories (%d)', + ), + 'feeds' => array( + 'over_max' => 'Vous avez atteint votre limite de flux (%d)', + ), +); diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index 8c2f90041..084bd7e07 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -5,71 +5,117 @@ */ /** - * La classe Translate se charge de la traduction - * Utilise les fichiers du répertoire /app/i18n/ + * This class is used for the internationalization. + * It uses files in `./app/i18n/` */ class Minz_Translate { /** - * $language est la langue à afficher + * $lang_name is the name of the current language to use. */ - private static $language; - + private static $lang_name; + + /** + * $lang_path is the pathname of i18n files (e.g. ./app/i18n/en/). + */ + private static $lang_path; + /** - * $translates est le tableau de correspondance - * $key => $traduction + * $translates is a cache for i18n translation. */ private static $translates = array(); - + /** - * Inclus le fichier de langue qui va bien - * l'enregistre dans $translates + * Load $lang_name and $lang_path based on configuration and selected language. */ public static function init() { $l = Minz_Configuration::language(); - self::$language = Minz_Session::param('language', $l); - - $l_path = APP_PATH . '/i18n/' . self::$language . '.php'; - - if (file_exists($l_path)) { - self::$translates = include($l_path); - } + self::$lang_name = Minz_Session::param('language', $l); + self::$lang_path = APP_PATH . '/i18n/' . self::$lang_name . '/'; } - + /** - * Alias de init + * Alias for init(). */ public static function reset() { self::init(); } - + /** - * Traduit une clé en sa valeur du tableau $translates - * @param $key la clé à traduire - * @return la valeur correspondante à la clé - * > si non présente dans le tableau, on retourne la clé elle-même + * Translate a key into its corresponding value based on selected language. + * @param $key the key to translate. + * @param additional parameters for variable keys. + * @return the value corresponding to the key. + * If no value is found, return the key itself. */ public static function t($key) { - $translate = $key; - - if (isset(self::$translates[$key])) { - $translate = self::$translates[$key]; + $group = explode('.', $key); + + if (count($group) < 2) { + // Minz_Log::debug($key . ' is not in a valid format'); + $top_level = 'gen'; + } else { + $top_level = array_shift($group); } + $filename = self::$lang_path . $top_level . '.php'; + + // Try to load the i18n file if it's not done yet. + if (!isset(self::$translates[$top_level])) { + if (!file_exists($filename)) { + Minz_Log::debug($top_level . ' is not a valid top level key'); + return $key; + } + + self::$translates[$top_level] = include($filename); + } + + // Go through the i18n keys to get the correct translation value. + $translates = self::$translates[$top_level]; + $size_group = count($group); + $level_processed = 0; + $translation_value = $key; + foreach ($group as $i18n_level) { + $level_processed++; + if (!isset($translates[$i18n_level])) { + Minz_Log::debug($key . ' is not a valid key'); + return $key; + } + + if ($level_processed < $size_group) { + $translates = $translates[$i18n_level]; + } else { + $translation_value = $translates[$i18n_level]; + } + } + + if (is_array($translation_value)) { + if (isset($translation_value['_'])) { + $translation_value = $translation_value['_']; + } else { + Minz_Log::debug($key . ' is not a valid key'); + return $key; + } + } + + // Get the facultative arguments to replace i18n variables. $args = func_get_args(); unset($args[0]); - - return vsprintf($translate, $args); + + return vsprintf($translation_value, $args); } - + /** - * Retourne la langue utilisée actuellement - * @return la langue + * Return the current language. */ public static function language() { - return self::$language; + return self::$lang_name; } } + +/** + * Alias for Minz_Translate::t() + */ function _t($key) { $args = func_get_args(); unset($args[0]); -- cgit v1.2.3 From ddc50090b55cd744e24995c872435a3fed7092da Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 3 Dec 2014 16:58:12 +0100 Subject: Fix i18n for installation script See https://github.com/FreshRSS/FreshRSS/issues/334 --- app/install.php | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/app/install.php b/app/install.php index cfdd733ce..f54565c73 100644 --- a/app/install.php +++ b/app/install.php @@ -42,18 +42,12 @@ function param($key, $default = false) { // gestion internationalisation -$translates = array(); -$actual = 'en'; function initTranslate() { - global $translates; - global $actual; - - $actual = isset($_SESSION['language']) ? $_SESSION['language'] : getBetterLanguage('en'); - - $file = APP_PATH . '/i18n/' . $actual . '.php'; - if (file_exists($file)) { - $translates = array_merge($translates, include($file)); + if (!isset($_SESSION['language'])) { + $_SESSION['language'] = getBetterLanguage('en'); } + + Minz_Translate::init(); } function getBetterLanguage($fallback) { @@ -75,19 +69,6 @@ function availableLanguages() { ); } -function _t($key) { - global $translates; - $translate = $key; - if (isset($translates[$key])) { - $translate = $translates[$key]; - } - - $args = func_get_args(); - unset($args[0]); - - return vsprintf($translate, $args); -} - /*** SAUVEGARDES ***/ function saveLanguage() { -- cgit v1.2.3 From 7161eacb4042de4fd994b727800038d3ec286374 Mon Sep 17 00:00:00 2001 From: Alwaysin Date: Wed, 3 Dec 2014 23:42:38 +0100 Subject: Add i18n (french) for auto_remove_article #2 Reference : #694 --- app/i18n/fr/gen.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 8a4a9750b..0b2395bd0 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -62,6 +62,7 @@ return array( 'author' => 'Auteur', 'auto_load_more' => 'Charger les articles suivants en bas de page', 'auto_read_when' => 'Marquer un article comme lu…', + 'auto_remove_article' => 'Cacher les articles après lecture', 'auto_share' => 'Partager', 'auto_share_help' => 'S’il n’y a qu’un mode de partage, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', 'back_to_rss_feeds' => '← Retour à vos flux RSS', -- cgit v1.2.3 From 93af0cf61e6e9368888eedb2d2e36397da3f87bd Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 3 Dec 2014 18:13:59 -0500 Subject: Fix behavior when marking an article as unread --- p/scripts/main.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/p/scripts/main.js b/p/scripts/main.js index 19eba206d..32cf55a3c 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -144,6 +144,7 @@ function mark_read(active, only_not_read) { inc = 0; if (active.hasClass("not_read")) { active.removeClass("not_read"); + hide_article(active); inc--; } else if (only_not_read !== true || active.hasClass("not_read")) { active.addClass("not_read"); @@ -231,14 +232,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(); - } + hide_article(old_active); } else { new_active.toggleClass('active'); } @@ -283,6 +277,17 @@ function toggleContent(new_active, old_active) { } } +function hide_article(article) { + if (context['auto_remove_article'] && !article.hasClass('not_read')) { + var p = article.prev(); + var n = article.next(); + if (p.hasClass('day') && n.hasClass('day')) { + p.remove(); + } + article.remove(); + } +} + function prev_entry() { var old_active = $(".flux.current"), new_active = old_active.length === 0 ? $(".flux:last") : old_active.prevAll(".flux:first"); @@ -690,9 +695,6 @@ function init_stream(divStream) { divStream.on('click', '.flux a.read', function () { var active = $(this).parents(".flux"); mark_read(active, false); - if (context['auto_remove_article']) { - active.remove(); - } return false; }); -- cgit v1.2.3 From 5617911644bf85718430bb096c4ae9a0a0e8c75c Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 3 Dec 2014 18:35:42 -0500 Subject: Revert "Fix behavior when marking an article as unread" This reverts commit 93af0cf61e6e9368888eedb2d2e36397da3f87bd. --- p/scripts/main.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/p/scripts/main.js b/p/scripts/main.js index 32cf55a3c..19eba206d 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -144,7 +144,6 @@ function mark_read(active, only_not_read) { inc = 0; if (active.hasClass("not_read")) { active.removeClass("not_read"); - hide_article(active); inc--; } else if (only_not_read !== true || active.hasClass("not_read")) { active.addClass("not_read"); @@ -232,7 +231,14 @@ function toggleContent(new_active, old_active) { } old_active.removeClass("active current"); new_active.addClass("current"); - hide_article(old_active); + 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(); + } } else { new_active.toggleClass('active'); } @@ -277,17 +283,6 @@ function toggleContent(new_active, old_active) { } } -function hide_article(article) { - if (context['auto_remove_article'] && !article.hasClass('not_read')) { - var p = article.prev(); - var n = article.next(); - if (p.hasClass('day') && n.hasClass('day')) { - p.remove(); - } - article.remove(); - } -} - function prev_entry() { var old_active = $(".flux.current"), new_active = old_active.length === 0 ? $(".flux:last") : old_active.prevAll(".flux:first"); @@ -695,6 +690,9 @@ function init_stream(divStream) { divStream.on('click', '.flux a.read', function () { var active = $(this).parents(".flux"); mark_read(active, false); + if (context['auto_remove_article']) { + active.remove(); + } return false; }); -- cgit v1.2.3 From 53410887c94157f3d11f2c30d92ff5d3d8a3a9bd Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Wed, 3 Dec 2014 18:57:53 -0500 Subject: Fix behavior to hide articles I do not like it since it is partly duplicated. We need to find something better. --- p/scripts/main.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/p/scripts/main.js b/p/scripts/main.js index 19eba206d..e48630d89 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -689,10 +689,15 @@ function init_stream(divStream) { divStream.on('click', '.flux a.read', function () { var active = $(this).parents(".flux"); - mark_read(active, false); - if (context['auto_remove_article']) { + 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(); } + mark_read(active, false); return false; }); -- cgit v1.2.3 From 86f69ca396572ca4d7668a33e84cb4f3b523fc4e Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 19:33:29 +0100 Subject: First draft for the new extension feature - Only system extensions can be loaded for the moment by adding them in the config.php file. - Remove previous system (it will be added properly in the new system in the next step). --- app/FreshRSS.php | 42 +++++------ constants.php | 3 +- extensions/README.md | 15 ++++ extensions/Read-me.txt | 15 ---- lib/Minz/Configuration.php | 10 +++ lib/Minz/Extension.php | 96 +++++++++++++++++++++++++ lib/Minz/ExtensionException.php | 15 ++++ lib/Minz/ExtensionManager.php | 150 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 310 insertions(+), 36 deletions(-) create mode 100644 extensions/README.md delete mode 100644 extensions/Read-me.txt create mode 100644 lib/Minz/Extension.php create mode 100644 lib/Minz/ExtensionException.php create mode 100644 lib/Minz/ExtensionManager.php diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 6114a5d1a..2db811a71 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -6,6 +6,9 @@ class FreshRSS extends Minz_FrontController { Minz_Session::init('FreshRSS'); } + // Load list of extensions and initialize the "system" ones. + Minz_ExtensionManager::init(); + // Need to be called just after session init because it initializes // current user. FreshRSS_Auth::init(); @@ -32,7 +35,6 @@ class FreshRSS extends Minz_FrontController { $this->loadStylesAndScripts(); $this->loadNotifications(); - $this->loadExtensions(); } private function loadStylesAndScripts() { @@ -74,23 +76,23 @@ class FreshRSS extends Minz_FrontController { } } - private function loadExtensions() { - $extensionPath = FRESHRSS_PATH . '/extensions/'; - //TODO: Add a preference to load only user-selected extensions - foreach (scandir($extensionPath) as $key => $extension) { - if (ctype_alpha($extension)) { - $mtime = @filemtime($extensionPath . $extension . '/style.css'); - if ($mtime !== false) { - Minz_View::appendStyle(Minz_Url::display('/ext.php?c&e=' . $extension . '&' . $mtime)); - } - $mtime = @filemtime($extensionPath . $extension . '/script.js'); - if ($mtime !== false) { - Minz_View::appendScript(Minz_Url::display('/ext.php?j&e=' . $extension . '&' . $mtime)); - } - if (file_exists($extensionPath . $extension . '/module.php')) { - //TODO: include - } - } - } - } + // private function loadExtensions() { + // $extensionPath = FRESHRSS_PATH . '/extensions/'; + // //TODO: Add a preference to load only user-selected extensions + // foreach (scandir($extensionPath) as $key => $extension) { + // if (ctype_alpha($extension)) { + // $mtime = @filemtime($extensionPath . $extension . '/style.css'); + // if ($mtime !== false) { + // Minz_View::appendStyle(Minz_Url::display('/ext.php?c&e=' . $extension . '&' . $mtime)); + // } + // $mtime = @filemtime($extensionPath . $extension . '/script.js'); + // if ($mtime !== false) { + // Minz_View::appendScript(Minz_Url::display('/ext.php?j&e=' . $extension . '&' . $mtime)); + // } + // if (file_exists($extensionPath . $extension . '/module.php')) { + // //TODO: include + // } + // } + // } + // } } diff --git a/constants.php b/constants.php index f66a012b0..999e703ba 100644 --- a/constants.php +++ b/constants.php @@ -20,6 +20,7 @@ define('FRESHRSS_PATH', dirname(__FILE__)); define('CACHE_PATH', DATA_PATH . '/cache'); define('LIB_PATH', FRESHRSS_PATH . '/lib'); - define('APP_PATH', FRESHRSS_PATH . '/app'); + define('APP_PATH', FRESHRSS_PATH . '/app'); + define('EXTENSIONS_PATH', FRESHRSS_PATH . '/extensions'); define('TMP_PATH', sys_get_temp_dir()); diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 000000000..e7b66d5bc --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,15 @@ +== FreshRSS extensions == + +You may place in this directory some custom extensions for FreshRSS. + +The structure must be: + +./FreshRSS/extensions/ + ./NameOfExtensionAlphanumeric/ + ./style.css + ./script.js + ./module.php + +Each file is optional. + +The name of non-official extensions should start by an 'x'. diff --git a/extensions/Read-me.txt b/extensions/Read-me.txt deleted file mode 100644 index e7b66d5bc..000000000 --- a/extensions/Read-me.txt +++ /dev/null @@ -1,15 +0,0 @@ -== FreshRSS extensions == - -You may place in this directory some custom extensions for FreshRSS. - -The structure must be: - -./FreshRSS/extensions/ - ./NameOfExtensionAlphanumeric/ - ./style.css - ./script.js - ./module.php - -Each file is optional. - -The name of non-official extensions should start by an 'x'. diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 6cbc9fc0b..4d3ab0964 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -69,6 +69,8 @@ class Minz_Configuration { 'max_categories' => Minz_Configuration::MAX_SMALL_INT, ); + private static $extensions_enabled = array(); + /* * Getteurs */ @@ -133,6 +135,9 @@ class Minz_Configuration { public static function unsafeAutologinEnabled() { return self::$unsafe_autologin_enabled; } + public static function extensionsEnabled() { + return self::$extensions_enabled; + } public static function _allowAnonymous($allow = false) { self::$allow_anonymous = ((bool)$allow) && self::canLogIn(); @@ -338,6 +343,11 @@ class Minz_Configuration { } } + // Extensions + if (isset($ini_array['extensions']) && is_array($ini_array['extensions'])) { + self::$extensions_enabled = $ini_array['extensions']; + } + // Base de données if (isset ($ini_array['db'])) { $db = $ini_array['db']; diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php new file mode 100644 index 000000000..f442344a3 --- /dev/null +++ b/lib/Minz/Extension.php @@ -0,0 +1,96 @@ +name = $meta_info['name']; + $this->entrypoint = $meta_info['entrypoint']; + $this->path = $meta_info['path']; + $this->author = isset($meta_info['author']) ? $meta_info['author'] : ''; + $this->description = isset($meta_info['description']) ? $meta_info['description'] : ''; + $this->version = isset($meta_info['version']) ? $meta_info['version'] : '0.1'; + $this->setType(isset($meta_info['type']) ? $meta_info['type'] : 'user'); + } + + /** + * Used when installing an extension (e.g. update the database scheme). + * + * It must be redefined by child classes. + */ + public function install() {} + + /** + * Used when uninstalling an extension (e.g. revert the database scheme to + * cancel changes from install). + * + * It must be redefined by child classes. + */ + public function uninstall() {} + + /** + * Call at the initialization of the extension (i.e. when the extension is + * enabled by the extension manager). + * + * It must be redefined by child classes. + */ + public function init() {} + + /** + * Getters and setters. + */ + public function getName() { + return $this->name; + } + public function getEntrypoint() { + return $this->entrypoint; + } + public function getAuthor() { + return $this->author; + } + public function getDescription() { + return $this->description; + } + public function getVersion() { + return $this->version; + } + public function getType() { + return $this->type; + } + private function setType($type) { + if (!in_array($type, self::$authorized_types)) { + throw new Minz_ExtensionException('invalid `type` info', $this->name); + } + $this->type = $type; + } +} diff --git a/lib/Minz/ExtensionException.php b/lib/Minz/ExtensionException.php new file mode 100644 index 000000000..647f1a9b9 --- /dev/null +++ b/lib/Minz/ExtensionException.php @@ -0,0 +1,15 @@ +Extension where + * must match with the entry point in metadata.json. This class must + * inherit from Minz_Extension class. + */ + public static function init() { + $list_potential_extensions = array_values(array_diff( + scandir(EXTENSIONS_PATH), + array('..', '.') + )); + + self::$ext_auto_enabled = Minz_Configuration::extensionsEnabled(); + + foreach ($list_potential_extensions as $ext_dir) { + $ext_pathname = EXTENSIONS_PATH . '/' . $ext_dir; + $metadata_filename = $ext_pathname . '/' . self::$ext_metaname; + + // Try to load metadata file. + if (!file_exists($metadata_filename)) { + // No metadata file? Invalid! + continue; + } + $meta_raw_content = file_get_contents($metadata_filename); + $meta_json = json_decode($meta_raw_content, true); + if (!$meta_json || !self::is_valid_metadata($meta_json)) { + // metadata.json is not a json file? Invalid! + // or metadata.json is invalid (no required information), invalid! + Minz_Log::warning('`' . $metadata_filename . '` is not a valid metadata file'); + continue; + } + + $meta_json['path'] = $ext_pathname; + + // Try to load extension itself + $extension = self::load($meta_json); + if (!is_null($extension)) { + self::register($extension); + } + } + } + + /** + * Indicates if the given parameter is a valid metadata array. + * + * Required fields are: + * - `name`: the name of the extension + * - `entry_point`: a class name to load the extension source code + * If the extension class name is `TestExtension`, entry point will be `Test`. + * `entry_point` must be composed of alphanumeric characters. + * + * @param $meta is an array of values. + * @return true if the array is valid, false else. + */ + public static function is_valid_metadata($meta) { + return !(empty($meta['name']) || + empty($meta['entrypoint']) || + !ctype_alnum($meta['entrypoint'])); + } + + /** + * Load the extension source code based on info metadata. + * + * @param $info an array containing information about extension. + * @return an extension inheriting from Minz_Extension. + */ + public static function load($info) { + $entry_point_filename = $info['path'] . '/' . self::$ext_entry_point; + $ext_class_name = $info['entrypoint'] . 'Extension'; + + include($entry_point_filename); + + // Test if the given extension class exists. + if (!class_exists($ext_class_name)) { + Minz_Log::warning('`' . $ext_class_name . + '` cannot be found in `' . $entry_point_filename . '`'); + return null; + } + + // Try to load the class. + $extension = null; + try { + $extension = new $ext_class_name($info); + } catch (Minz_ExtensionException $e) { + // We cannot load the extension? Invalid! + Minz_Log::warning('In `' . $metadata_filename . '`: ' . $e->getMessage()); + return null; + } + + // Test if class is correct. + if (!($extension instanceof Minz_Extension)) { + Minz_Log::warning('`' . $ext_class_name . + '` is not an instance of `Minz_Extension`'); + return null; + } + + return $extension; + } + + /** + * Add the extension to the list of the known extensions ($ext_list). + * + * If the extension is present in $ext_auto_enabled and if its type is "system", + * it will be enabled in the same time. + * + * @param $ext a valid extension. + */ + public static function register($ext) { + $name = $ext->getName(); + self::$ext_list[$name] = $ext; + + if ($ext->getType() === 'system' && + in_array($name, self::$ext_auto_enabled)) { + self::enable($ext->getName()); + } + } + + /** + * Enable an extension so it will be called when necessary. + * + * The extension init() method will be called. + * + * @param $ext_name is the name of a valid extension present in $ext_list. + */ + public static function enable($ext_name) { + if (isset(self::$ext_list[$ext_name])) { + $ext = self::$ext_list[$ext_name]; + self::$ext_list_enabled[$ext_name] = $ext; + $ext->init(); + } + } +} -- cgit v1.2.3 From 1086ba4a2bbe43a0101105624f831516b59ba9e9 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 19:47:43 +0100 Subject: Enable extensions for users --- app/FreshRSS.php | 6 +++++- app/Models/Configuration.php | 8 ++++++++ lib/Minz/ExtensionManager.php | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 2db811a71..166ee1709 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -6,7 +6,7 @@ class FreshRSS extends Minz_FrontController { Minz_Session::init('FreshRSS'); } - // Load list of extensions and initialize the "system" ones. + // Load list of extensions and enable the "system" ones. Minz_ExtensionManager::init(); // Need to be called just after session init because it initializes @@ -29,6 +29,10 @@ class FreshRSS extends Minz_FrontController { // Load context and configuration. FreshRSS_Context::init(); + // Enable extensions for the current user. + $ext_list = FreshRSS_Context::$conf->extensions_enabled; + Minz_ExtensionManager::enable_by_list($ext_list); + // Init i18n. Minz_Session::_param('language', FreshRSS_Context::$conf->language); Minz_Translate::init(); diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 8668470b0..13ce43990 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -64,6 +64,7 @@ class FreshRSS_Configuration { 'sharing' => array(), 'queries' => array(), 'html5_notif_timeout' => 0, + 'extensions_enabled' => array(), ); private $available_languages = array( @@ -342,4 +343,11 @@ class FreshRSS_Configuration { public function _bottomline_link($value) { $this->data['bottomline_link'] = ((bool)$value) && $value !== 'no'; } + + public function _extensions_enabled($value) { + if (!is_array($value)) { + $value = array($value); + } + $this->data['extensions_enabled'] = $value; + } } diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index ae648fe0d..789557b9e 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -147,4 +147,15 @@ class Minz_ExtensionManager { $ext->init(); } } + + /** + * Enable a list of extensions. + * + * @param $ext_list the names of extensions we want to load. + */ + public static function enable_by_list($ext_list) { + foreach ($ext_list as $ext_name) { + self::enable($ext_name); + } + } } -- cgit v1.2.3 From 0316badf649ef285f068847ef094ace80dd51290 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 19:52:07 +0100 Subject: Update gitignore for extensions --- extensions/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/.gitignore b/extensions/.gitignore index d93e5e396..cd5592022 100644 --- a/extensions/.gitignore +++ b/extensions/.gitignore @@ -1 +1 @@ -/[xX] +[xX]* -- cgit v1.2.3 From f9b037742a0aeb49cab86782d1a59913c2de47bf Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 20:41:01 +0100 Subject: Update ext.php to serve any file from extensions Add an extension->getFileUrl() method to facilitate url generation --- lib/Minz/Extension.php | 23 +++++++++++++++++++++++ p/ext.php | 37 +++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index f442344a3..72a375a6d 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -75,6 +75,9 @@ class Minz_Extension { public function getEntrypoint() { return $this->entrypoint; } + public function getPath() { + return $this->path; + } public function getAuthor() { return $this->author; } @@ -93,4 +96,24 @@ class Minz_Extension { } $this->type = $type; } + + /** + * Return the url for a given file. + * + * @param $filename name of the file to serve. + * @param $type the type (js or css) of the file to serve. + * @return the url corresponding to the file. + */ + public function getFileUrl($filename, $type) { + $dir = end(explode('/', $this->path)); + $file_name_url = urlencode($dir . '/' . $filename); + + $absolute_path = $this->path . '/' . $filename; + $mtime = @filemtime($absolute_path); + + $url = '/ext.php?f=' . $file_name_url . + '&t=' . $type . + '&' . $mtime; + return Minz_Url::display($url); + } } diff --git a/p/ext.php b/p/ext.php index a1dde2f93..39c224a84 100644 --- a/p/ext.php +++ b/p/ext.php @@ -1,32 +1,33 @@ Date: Thu, 4 Dec 2014 20:43:05 +0100 Subject: Remove old code for extensions --- app/FreshRSS.php | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 166ee1709..dc7d0b375 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -79,24 +79,4 @@ class FreshRSS extends Minz_FrontController { Minz_Session::_param('notification'); } } - - // private function loadExtensions() { - // $extensionPath = FRESHRSS_PATH . '/extensions/'; - // //TODO: Add a preference to load only user-selected extensions - // foreach (scandir($extensionPath) as $key => $extension) { - // if (ctype_alpha($extension)) { - // $mtime = @filemtime($extensionPath . $extension . '/style.css'); - // if ($mtime !== false) { - // Minz_View::appendStyle(Minz_Url::display('/ext.php?c&e=' . $extension . '&' . $mtime)); - // } - // $mtime = @filemtime($extensionPath . $extension . '/script.js'); - // if ($mtime !== false) { - // Minz_View::appendScript(Minz_Url::display('/ext.php?j&e=' . $extension . '&' . $mtime)); - // } - // if (file_exists($extensionPath . $extension . '/module.php')) { - // //TODO: include - // } - // } - // } - // } } -- cgit v1.2.3 From 2a74b9e9a69fcfe74b513c487430669540ef18fc Mon Sep 17 00:00:00 2001 From: Alwaysin Date: Fri, 5 Dec 2014 09:41:54 +0100 Subject: Add i18n (english) for auto_remove_article #2 Reference : https://github.com/FreshRSS/FreshRSS/pull/694 --- app/i18n/en/gen.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 9e06357bc..5dc2c3e28 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -62,6 +62,7 @@ return array( 'author' => 'Author', 'auto_load_more' => 'Load next articles at the page bottom', 'auto_read_when' => 'Mark article as read…', + 'auto_remove_article' => 'Hide articles after reading' 'auto_share' => 'Share', 'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.', 'back_to_rss_feeds' => '← Go back to your RSS feeds', -- cgit v1.2.3 From a2da70fd119cc43438f8dd88de54a7d19fafbe1a Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 10:51:34 +0100 Subject: Fix security hole from ext.php script. Now, ext.php can only serve file under a EXTENSIONS_PATH/ext_dir/static/ directory. A 400 Bad Request error will be returned for other files. See https://github.com/FreshRSS/FreshRSS/issues/252 And https://github.com/FreshRSS/FreshRSS/commit/f9b037742a0aeb49cab86782d1a59913c2de47b --- lib/Minz/Extension.php | 4 ++-- p/ext.php | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 72a375a6d..ecf510ea2 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -106,9 +106,9 @@ class Minz_Extension { */ public function getFileUrl($filename, $type) { $dir = end(explode('/', $this->path)); - $file_name_url = urlencode($dir . '/' . $filename); + $file_name_url = urlencode($dir . '/static/' . $filename); - $absolute_path = $this->path . '/' . $filename; + $absolute_path = $this->path . '/static/' . $filename; $mtime = @filemtime($absolute_path); $url = '/ext.php?f=' . $file_name_url . diff --git a/p/ext.php b/p/ext.php index 39c224a84..5c9f9125f 100644 --- a/p/ext.php +++ b/p/ext.php @@ -7,10 +7,42 @@ if (!isset($_GET['f']) || require('../constants.php'); +/** + * Check if a file can be served by ext.php. A valid file is under a + * EXTENSIONS_PATH/extension_name/static/ directory. + * + * You should sanitize path by using the realpath() function. + * + * @param $path the path to the file we want to serve. + * @return true if it can be served, false else. + * + */ +function is_valid_path($path) { + // It must be under the extension path. + $in_ext_path = (substr($path, 0, strlen(EXTENSIONS_PATH)) === EXTENSIONS_PATH); + if (!$in_ext_path) { + return false; + } + + // File to serve must be under a `ext_dir/static/` directory. + $path_relative_to_ext = substr($path, strlen(EXTENSIONS_PATH) + 1); + $path_splitted = explode('/', $path_relative_to_ext); + if (count($path_splitted) < 3 || $path_splitted[1] !== 'static') { + return false; + } + + return true; +} + $file_name = urldecode($_GET['f']); $file_type = $_GET['t']; -$absolute_filename = EXTENSIONS_PATH . '/' . $file_name; +$absolute_filename = realpath(EXTENSIONS_PATH . '/' . $file_name); + +if (!is_valid_path($absolute_filename)) { + header('HTTP/1.1 400 Bad Request'); + die(); +} switch ($file_type) { case 'css': -- cgit v1.2.3 From 6a706c95df557d1897608c89d3f5e0d0e9cd20ac Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Fri, 5 Dec 2014 07:07:23 -0500 Subject: fix typo --- app/i18n/en/gen.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 5dc2c3e28..a7f51e8f9 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -62,7 +62,7 @@ return array( 'author' => 'Author', 'auto_load_more' => 'Load next articles at the page bottom', 'auto_read_when' => 'Mark article as read…', - 'auto_remove_article' => 'Hide articles after reading' + 'auto_remove_article' => 'Hide articles after reading', 'auto_share' => 'Share', 'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.', 'back_to_rss_feeds' => '← Go back to your RSS feeds', -- cgit v1.2.3 From 9fc60317eecba785b66011f04b9a5150296f2df6 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 14:17:02 +0100 Subject: First draft for listing and manipulate extensions See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 43 +++++++++++++++++++++++++++++++++ app/layout/aside_configure.phtml | 4 +++ app/layout/header.phtml | 1 + app/views/extension/index.phtml | 35 +++++++++++++++++++++++++++ lib/Minz/Extension.php | 20 +++++++++++++++ lib/Minz/ExtensionManager.php | 17 +++++++++++++ 6 files changed, 120 insertions(+) create mode 100644 app/Controllers/extensionController.php create mode 100644 app/views/extension/index.phtml diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php new file mode 100644 index 000000000..504be56d3 --- /dev/null +++ b/app/Controllers/extensionController.php @@ -0,0 +1,43 @@ +view->extension_list = Minz_ExtensionManager::list_extensions(); + } + + public function configureAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } + } + + public function enableAction() { + + } + + public function disableAction() { + + } + + public function removeAction() { + + } +} diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml index 53c52d3e3..f7f3617c4 100644 --- a/app/layout/aside_configure.phtml +++ b/app/layout/aside_configure.phtml @@ -22,6 +22,10 @@ Minz_Request::actionName() === 'profile'? ' active' : ''; ?>"> +
  • + +
  • +
  • diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml new file mode 100644 index 000000000..c6b7c84a1 --- /dev/null +++ b/app/views/extension/index.phtml @@ -0,0 +1,35 @@ +partial('aside_configure'); ?> + +
    + + +

    + + extension_list)) { ?> + + extension_list as $ext) { ?> +
      +
    • + getName()); ?> +
      + + is_enabled()) { ?> + + + + + + + +
      +
    • +
    • getName(); ?>
    • +
    + + +

    + +
    + + +
    diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index ecf510ea2..a1fdd659b 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -17,6 +17,8 @@ class Minz_Extension { 'user', ); + private $is_enabled; + /** * The constructor to assign specific information to the extension. * @@ -41,6 +43,8 @@ class Minz_Extension { $this->description = isset($meta_info['description']) ? $meta_info['description'] : ''; $this->version = isset($meta_info['version']) ? $meta_info['version'] : '0.1'; $this->setType(isset($meta_info['type']) ? $meta_info['type'] : 'user'); + + $this->is_enabled = false; } /** @@ -66,6 +70,22 @@ class Minz_Extension { */ public function init() {} + /** + * Set the current extension to enable. + */ + public function enable() { + $this->is_enabled = true; + } + + /** + * Return if the extension is currently enabled. + * + * @return true if extension is enabled, false else. + */ + public function is_enabled() { + return $this->is_enabled; + } + /** * Getters and setters. */ diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 789557b9e..6c32ccf19 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -144,6 +144,7 @@ class Minz_ExtensionManager { if (isset(self::$ext_list[$ext_name])) { $ext = self::$ext_list[$ext_name]; self::$ext_list_enabled[$ext_name] = $ext; + $ext->enable(); $ext->init(); } } @@ -158,4 +159,20 @@ class Minz_ExtensionManager { self::enable($ext_name); } } + + + + /** + * Returns a list of extensions. + * + * @param $only_enabled if true returns only the enabled extensions (false by default). + * @return an array of extensions. + */ + public static function list_extensions($only_enabled = false) { + if ($only_enabled) { + return self::$ext_list_enabled; + } else { + return self::$ext_list; + } + } } -- cgit v1.2.3 From f8aa66152fcab24ae7cd9663dab0c0c96a45ca24 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 14:48:09 +0100 Subject: Give possibility to register a new Controller. - Add a Extension->registerController(name) method - Controllers must be written in extension_dir/controllers/nameController.php - Controllers must be named as FreshExtension_name_Controller - Controllers must extend Minz_ActionController See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/Dispatcher.php | 47 ++++++++++++++++++++++++++++++++++++++++++++--- lib/Minz/Extension.php | 13 +++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index f62a92911..66789a3d3 100644 --- a/lib/Minz/Dispatcher.php +++ b/lib/Minz/Dispatcher.php @@ -15,6 +15,7 @@ class Minz_Dispatcher { /* singleton */ private static $instance = null; private static $needsReset; + private static $registrations = array(); private $controller; @@ -38,7 +39,7 @@ class Minz_Dispatcher { self::$needsReset = false; try { - $this->createController ('FreshRSS_' . Minz_Request::controllerName () . '_Controller'); + $this->createController (Minz_Request::controllerName ()); $this->controller->init (); $this->controller->firstAction (); if (!self::$needsReset) { @@ -73,8 +74,11 @@ class Minz_Dispatcher { * > pas une instance de ActionController */ private function createController ($controller_name) { - $filename = APP_PATH . self::CONTROLLERS_PATH_NAME . '/' - . $controller_name . '.php'; + if (self::isRegistered($controller_name)) { + $controller_name = self::loadController($controller_name); + } else { + $controller_name = 'FreshRSS_' . $controller_name . '_Controller'; + } if (!class_exists ($controller_name)) { throw new Minz_ControllerNotExistException ( @@ -114,4 +118,41 @@ class Minz_Dispatcher { $action_name )); } + + /** + * Register a controller file. + * + * @param $base_name the base name of the controller (i.e. ./?c=) + * @param $controller_name the name of the controller (e.g. HelloWorldController). + * @param $filename the file which contains the controller. + */ + public static function registerController($base_name, $controller_name, $filename) { + if (file_exists($filename)) { + self::$registrations[$base_name] = array( + $controller_name, + $filename, + ); + } + } + + /** + * Return if a controller is registered. + * + * @param $base_name the base name of the controller. + * @return true if the controller has been registered, false else. + */ + public static function isRegistered($base_name) { + return isset(self::$registrations[$base_name]); + } + + /** + * Load a controller file (include) and return its name. + * + * @param $base_name the base name of the controller. + */ + private static function loadController($base_name) { + list($controller_name, $filename) = self::$registrations[$base_name]; + include($filename); + return $controller_name; + } } diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index a1fdd659b..ad3465640 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -136,4 +136,17 @@ class Minz_Extension { '&' . $mtime; return Minz_Url::display($url); } + + /** + * Register a controller in the Dispatcher. + * + * @param @base_name the base name of the controller. Final name will be: + * FreshExtension__Controller. + */ + public function registerController($base_name) { + $controller_name = 'FreshExtension_' . $base_name . '_Controller'; + $filename = $this->path . '/controllers/' . $base_name . 'Controller.php'; + + Minz_Dispatcher::registerController($base_name, $controller_name, $filename); + } } -- cgit v1.2.3 From c6a682deb94111c1e14cf10e565da3f4214f02dc Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 15:27:56 +0100 Subject: Extensions can define new views - View base pathname is set to the extension directory - An extension can now override an existing controller / view See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/Dispatcher.php | 40 +++++++++++++++++++++++----------------- lib/Minz/Extension.php | 5 +---- lib/Minz/View.php | 10 +++++++--- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index 66789a3d3..edd59c7cc 100644 --- a/lib/Minz/Dispatcher.php +++ b/lib/Minz/Dispatcher.php @@ -68,16 +68,17 @@ class Minz_Dispatcher { /** * Instancie le Controller - * @param $controller_name le nom du controller à instancier + * @param $base_name le nom du controller à instancier * @exception ControllerNotExistException le controller n'existe pas * @exception ControllerNotActionControllerException controller n'est * > pas une instance de ActionController */ - private function createController ($controller_name) { - if (self::isRegistered($controller_name)) { - $controller_name = self::loadController($controller_name); + private function createController ($base_name) { + if (self::isRegistered($base_name)) { + self::loadController($base_name); + $controller_name = 'FreshExtension_' . $base_name . '_Controller'; } else { - $controller_name = 'FreshRSS_' . $controller_name . '_Controller'; + $controller_name = 'FreshRSS_' . $base_name . '_Controller'; } if (!class_exists ($controller_name)) { @@ -94,6 +95,10 @@ class Minz_Dispatcher { Minz_Exception::ERROR ); } + + if (self::isRegistered($base_name)) { + $this->setViewPath($this->controller, $base_name); + } } /** @@ -123,15 +128,11 @@ class Minz_Dispatcher { * Register a controller file. * * @param $base_name the base name of the controller (i.e. ./?c=) - * @param $controller_name the name of the controller (e.g. HelloWorldController). - * @param $filename the file which contains the controller. + * @param $base_path the base path where we should look into to find info. */ - public static function registerController($base_name, $controller_name, $filename) { - if (file_exists($filename)) { - self::$registrations[$base_name] = array( - $controller_name, - $filename, - ); + public static function registerController($base_name, $base_path) { + if (!self::isRegistered($base_name)) { + self::$registrations[$base_name] = $base_path; } } @@ -146,13 +147,18 @@ class Minz_Dispatcher { } /** - * Load a controller file (include) and return its name. + * Load a controller file (include). * * @param $base_name the base name of the controller. */ private static function loadController($base_name) { - list($controller_name, $filename) = self::$registrations[$base_name]; - include($filename); - return $controller_name; + $base_path = self::$registrations[$base_name]; + $controller_filename = $base_path . '/controllers/' . $base_name . 'Controller.php'; + include($controller_filename); + } + + private static function setViewPath($controller, $base_name) { + $base_path = self::$registrations[$base_name]; + $controller->view()->setBasePathname($base_path); } } diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index ad3465640..5a61ba2e0 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -144,9 +144,6 @@ class Minz_Extension { * FreshExtension__Controller. */ public function registerController($base_name) { - $controller_name = 'FreshExtension_' . $base_name . '_Controller'; - $filename = $this->path . '/controllers/' . $base_name . 'Controller.php'; - - Minz_Dispatcher::registerController($base_name, $controller_name, $filename); + Minz_Dispatcher::registerController($base_name, $this->path); } } diff --git a/lib/Minz/View.php b/lib/Minz/View.php index b40448491..bdfbbe63c 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -12,6 +12,7 @@ class Minz_View { const LAYOUT_PATH_NAME = '/layout'; const LAYOUT_FILENAME = '/layout.phtml'; + private $base_pathname = APP_PATH; private $view_filename = ''; private $use_layout = null; @@ -35,12 +36,15 @@ class Minz_View { * Change le fichier de vue en fonction d'un controller / action */ public function change_view($controller_name, $action_name) { - $this->view_filename = APP_PATH - . self::VIEWS_PATH_NAME . '/' + $this->view_filename = self::VIEWS_PATH_NAME . '/' . $controller_name . '/' . $action_name . '.phtml'; } + public function setBasePathname($base_pathname) { + $this->base_pathname = $base_pathname; + } + /** * Construit la vue */ @@ -70,7 +74,7 @@ class Minz_View { * Affiche la Vue en elle-même */ public function render () { - if ((include($this->view_filename)) === false) { + if ((include($this->base_pathname . $this->view_filename)) === false) { Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } -- cgit v1.2.3 From a08c382e0651f22a7db06feba225f3d49289763d Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 15:20:20 +0100 Subject: Separate views registration from controllers one. - Add an Extension->registerViews() method. - Views are first searched in extension paths, then in APP_PATH. - It gives a way to override easily existing controllers / views. - Change include into an include_once in Dispatcher for new controllers. See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/Dispatcher.php | 6 +----- lib/Minz/Extension.php | 7 +++++++ lib/Minz/View.php | 28 ++++++++++++++++++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index edd59c7cc..125ce5757 100644 --- a/lib/Minz/Dispatcher.php +++ b/lib/Minz/Dispatcher.php @@ -95,10 +95,6 @@ class Minz_Dispatcher { Minz_Exception::ERROR ); } - - if (self::isRegistered($base_name)) { - $this->setViewPath($this->controller, $base_name); - } } /** @@ -154,7 +150,7 @@ class Minz_Dispatcher { private static function loadController($base_name) { $base_path = self::$registrations[$base_name]; $controller_filename = $base_path . '/controllers/' . $base_name . 'Controller.php'; - include($controller_filename); + include_once $controller_filename; } private static function setViewPath($controller, $base_name) { diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 5a61ba2e0..490a5c5cb 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -146,4 +146,11 @@ class Minz_Extension { public function registerController($base_name) { Minz_Dispatcher::registerController($base_name, $this->path); } + + /** + * Register the views in order to be accessible by the application. + */ + public function registerViews() { + Minz_View::addBasePathname($this->path); + } } diff --git a/lib/Minz/View.php b/lib/Minz/View.php index bdfbbe63c..1bc2e862d 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -12,10 +12,10 @@ class Minz_View { const LAYOUT_PATH_NAME = '/layout'; const LAYOUT_FILENAME = '/layout.phtml'; - private $base_pathname = APP_PATH; private $view_filename = ''; private $use_layout = null; + private static $base_pathnames = array(APP_PATH); private static $title = ''; private static $styles = array (); private static $scripts = array (); @@ -41,8 +41,15 @@ class Minz_View { . $action_name . '.phtml'; } - public function setBasePathname($base_pathname) { - $this->base_pathname = $base_pathname; + /** + * Add a base pathname to search views. + * + * New pathnames will be added at the beginning of the list. + * + * @param $base_pathname the new base pathname. + */ + public static function addBasePathname($base_pathname) { + array_unshift(self::$base_pathnames, $base_pathname); } /** @@ -74,7 +81,20 @@ class Minz_View { * Affiche la Vue en elle-même */ public function render () { - if ((include($this->base_pathname . $this->view_filename)) === false) { + $view_found = false; + + // We search the view in the list of base pathnames. Only the first view + // found is considered. + foreach (self::$base_pathnames as $base) { + $filename = $base . $this->view_filename; + if (file_exists($filename)) { + include $filename; + $view_found = true; + break; + } + } + + if (!$view_found) { Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } -- cgit v1.2.3 From 2e4682ebd451f8dd291e11141553add9164cbbef Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 16:17:11 +0100 Subject: Add enable / disable extension features See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 72 ++++++++++++++++++++++++++++++++- app/Models/Configuration.php | 12 ++++++ lib/Minz/ExtensionManager.php | 18 +++++++-- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 504be56d3..415f489a6 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -29,12 +29,80 @@ class FreshRSS_extension_Controller extends Minz_ActionController { } } + /** + * This action enables a disabled extension for the current user. + * + * System extensions can only be enabled by an administrator. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ public function enableAction() { - + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad('feedback.extension.not_found', $url_redirect); + } + + if ($ext->is_enabled()) { + Minz_Request::bad('feedback.extension.already_enabled', $url_redirect); + } + + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } + + $ext->install(); + + FreshRSS_Context::$conf->addExtension($ext_name); + FreshRSS_Context::$conf->save(); + + Minz_Request::good('feedback.extension.enabled', $url_redirect); + } + + Minz_Request::forward($url_redirect, true); } + /** + * This action disables an enabled extension for the current user. + * + * System extensions can only be disabled by an administrator. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ public function disableAction() { - + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad('feedback.extension.not_found', $url_redirect); + } + + if (!$ext->is_enabled()) { + Minz_Request::bad('feedback.extension.not_enabled', $url_redirect); + } + + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } + + $ext->uninstall(); + + FreshRSS_Context::$conf->removeExtension($ext_name); + FreshRSS_Context::$conf->save(); + + Minz_Request::good('feedback.extension.disabled', $url_redirect); + } + + Minz_Request::forward($url_redirect, true); } public function removeAction() { diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 13ce43990..83a00d4bb 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -350,4 +350,16 @@ class FreshRSS_Configuration { } $this->data['extensions_enabled'] = $value; } + public function removeExtension($ext_name) { + $this->data['extensions_enabled'] = array_diff( + $this->data['extensions_enabled'], + array($ext_name) + ); + } + public function addExtension($ext_name) { + $found = array_search($ext_name, $this->data['extensions_enabled']) !== false; + if (!$found) { + $this->data['extensions_enabled'][] = $ext_name; + } + } } diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 6c32ccf19..46e421bac 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -160,10 +160,8 @@ class Minz_ExtensionManager { } } - - /** - * Returns a list of extensions. + * Return a list of extensions. * * @param $only_enabled if true returns only the enabled extensions (false by default). * @return an array of extensions. @@ -175,4 +173,18 @@ class Minz_ExtensionManager { return self::$ext_list; } } + + /** + * Return an extension by its name. + * + * @param $ext_name the name of the extension. + * @return the corresponding extension or null if it doesn't exist. + */ + public static function find_extension($ext_name) { + if (!isset(self::$ext_list[$ext_name])) { + return null; + } + + return self::$ext_list[$ext_name]; + } } -- cgit v1.2.3 From eaaf8cdbf1e87ad22d25257eb99a4b80b579e661 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Sat, 6 Dec 2014 10:15:34 -0500 Subject: Add comments --- app/Models/EntryDAO.php | 77 +++++++++++++++++++++++++++++++++++++++++++ app/Models/EntryDAOSQLite.php | 45 +++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 5d2909c62..4d06ac028 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -80,6 +80,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return -1; } + /** + * Toggle favorite marker on one or more article + * + * @todo simplify the query by removing the str_repeat. I am pretty sure + * there is an other way to do that. + * + * @param integer|array $ids + * @param boolean $is_favorite + * @return false|integer + */ public function markFavorite($ids, $is_favorite = true) { if (!is_array($ids)) { $ids = array($ids); @@ -99,6 +109,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } } + /** + * Update the unread article cache held on every feed details. + * Depending on the parameters, it updates the cache on one feed, on all + * feeds from one category or on all feeds. + * + * @todo It can use the query builder refactoring to build that query + * + * @param false|integer $catId category ID + * @param false|integer $feedId feed ID + * @return boolean + */ protected function updateCacheUnreads($catId = false, $feedId = false) { $sql = 'UPDATE `' . $this->prefix . 'feed` f ' . 'LEFT OUTER JOIN (' @@ -129,6 +150,19 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } } + /** + * Toggle the read marker on one or more article. + * Then the cache is updated. + * + * @todo change the way the query is build because it seems there is + * unnecessary code in here. For instance, the part with the str_repeat. + * @todo remove code duplication. It seems the code is basically the + * same if it is an array or not. + * + * @param integer|array $ids + * @param boolean $is_read + * @return integer affected rows + */ public function markRead($ids, $is_read = true) { if (is_array($ids)) { //Many IDs at once (used by API) if (count($ids) < 6) { //Speed heuristics @@ -172,6 +206,27 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } } + /** + * Mark all entries as read depending on parameters. + * If $onlyFavorites is true, it is used when the user mark as read in + * the favorite pseudo-category. + * If $priorityMin is greater than 0, it is used when the user mark as + * read in the main feed pseudo-category. + * Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @todo refactor this method along with markReadCat and markReadFeed + * since they are all doing the same thing. I think we need to build a + * tool to generate the query instead of having queries all over the + * place. It will be reused also for the filtering making every thing + * separated. + * + * @param integer $idMax fail safe article ID + * @param boolean $onlyFavorites + * @param integer $priorityMin + * @return integer affected rows + */ public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { if ($idMax == 0) { $idMax = time() . '000000'; @@ -200,6 +255,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } + /** + * Mark all the articles in a category as read. + * There is a fail safe to prevent to mark as read articles that are + * loaded during the mark as read action. Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @param integer $id category ID + * @param integer $idMax fail safe article ID + * @return integer affected rows + */ public function markReadCat($id, $idMax = 0) { if ($idMax == 0) { $idMax = time() . '000000'; @@ -223,6 +289,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } + /** + * Mark all the articles in a feed as read. + * There is a fail safe to prevent to mark as read articles that are + * loaded during the mark as read action. Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @param integer $id feed ID + * @param integer $idMax fail safe article ID + * @return integer affected rows + */ public function markReadFeed($id, $idMax = 0) { if ($idMax == 0) { $idMax = time() . '000000'; diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 4a3fe24a2..bb1539e0c 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -31,6 +31,19 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } } + /** + * Toggle the read marker on one or more article. + * Then the cache is updated. + * + * @todo change the way the query is build because it seems there is + * unnecessary code in here. For instance, the part with the str_repeat. + * @todo remove code duplication. It seems the code is basically the + * same if it is an array or not. + * + * @param integer|array $ids + * @param boolean $is_read + * @return integer affected rows + */ public function markRead($ids, $is_read = true) { if (is_array($ids)) { //Many IDs at once (used by API) if (true) { //Speed heuristics //TODO: Not implemented yet for SQLite (so always call IDs one by one) @@ -69,6 +82,27 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } } + /** + * Mark all entries as read depending on parameters. + * If $onlyFavorites is true, it is used when the user mark as read in + * the favorite pseudo-category. + * If $priorityMin is greater than 0, it is used when the user mark as + * read in the main feed pseudo-category. + * Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @todo refactor this method along with markReadCat and markReadFeed + * since they are all doing the same thing. I think we need to build a + * tool to generate the query instead of having queries all over the + * place. It will be reused also for the filtering making every thing + * separated. + * + * @param integer $idMax fail safe article ID + * @param boolean $onlyFavorites + * @param integer $priorityMin + * @return integer affected rows + */ public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { if ($idMax == 0) { $idMax = time() . '000000'; @@ -95,6 +129,17 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { return $affected; } + /** + * Mark all the articles in a category as read. + * There is a fail safe to prevent to mark as read articles that are + * loaded during the mark as read action. Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @param integer $id category ID + * @param integer $idMax fail safe article ID + * @return integer affected rows + */ public function markReadCat($id, $idMax = 0) { if ($idMax == 0) { $idMax = time() . '000000'; -- cgit v1.2.3 From 4c888590e6f0fd89fc1dccebb5e815883eeaa54c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 16:39:10 +0100 Subject: Improve system/user types for extensions - system extensions can only be managed by an administrator - system extensions are loaded for all users (even if not logged) - user extensions are loaded for logged users only - system extensions loading is saved in global config.php file See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 42 ++++++++++++++++++++++----------- app/FreshRSS.php | 8 ++++--- app/views/extension/index.phtml | 4 ++++ lib/Minz/Configuration.php | 19 +++++++++++++-- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 415f489a6..e348d9f31 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -52,16 +52,23 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Request::bad('feedback.extension.already_enabled', $url_redirect); } - if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); - } + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $ext->install(); + + Minz_Configuration::addExtension($ext_name); + Minz_Configuration::writeFile(); - $ext->install(); + Minz_Request::good('feedback.extension.enabled', $url_redirect); + } elseif ($ext->getType() === 'user') { + $ext->install(); - FreshRSS_Context::$conf->addExtension($ext_name); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$conf->addExtension($ext_name); + FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.enabled', $url_redirect); + Minz_Request::good('feedback.extension.enabled', $url_redirect); + } else { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } } Minz_Request::forward($url_redirect, true); @@ -90,16 +97,23 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Request::bad('feedback.extension.not_enabled', $url_redirect); } - if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); - } + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $ext->uninstall(); + + Minz_Configuration::removeExtension($ext_name); + Minz_Configuration::writeFile(); - $ext->uninstall(); + Minz_Request::good('feedback.extension.disabled', $url_redirect); + } elseif ($ext->getType() === 'user') { + $ext->uninstall(); - FreshRSS_Context::$conf->removeExtension($ext_name); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$conf->removeExtension($ext_name); + FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.disabled', $url_redirect); + Minz_Request::good('feedback.extension.disabled', $url_redirect); + } else { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } } Minz_Request::forward($url_redirect, true); diff --git a/app/FreshRSS.php b/app/FreshRSS.php index dc7d0b375..b91dfcc46 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -29,9 +29,11 @@ class FreshRSS extends Minz_FrontController { // Load context and configuration. FreshRSS_Context::init(); - // Enable extensions for the current user. - $ext_list = FreshRSS_Context::$conf->extensions_enabled; - Minz_ExtensionManager::enable_by_list($ext_list); + // Enable extensions for the current (logged) user. + if (FreshRSS_Auth::hasAccess()) { + $ext_list = FreshRSS_Context::$conf->extensions_enabled; + Minz_ExtensionManager::enable_by_list($ext_list); + } // Init i18n. Minz_Session::_param('language', FreshRSS_Context::$conf->language); diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index c6b7c84a1..0be03d7b5 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -10,6 +10,7 @@ extension_list as $ext) { ?>
    • + getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> getName()); ?>
      @@ -22,6 +23,9 @@
      + + +
    • getName(); ?>
    diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 4d3ab0964..4a3221ef5 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -165,6 +165,19 @@ class Minz_Configuration { self::$unsafe_autologin_enabled = (bool)$value; } + public function removeExtension($ext_name) { + self::$extensions_enabled = array_diff( + self::$extensions_enabled, + array($ext_name) + ); + } + public function addExtension($ext_name) { + $found = array_search($ext_name, self::$extensions_enabled) !== false; + if (!$found) { + self::$extensions_enabled[] = $ext_name; + } + } + /** * Initialise les variables de configuration * @exception Minz_FileNotExistException si le CONF_PATH_NAME n'existe pas @@ -197,6 +210,7 @@ class Minz_Configuration { ), 'limits' => self::$limits, 'db' => self::$db, + 'extensions_enabled' => self::$extensions_enabled, ); @rename(DATA_PATH . self::CONF_PATH_NAME, DATA_PATH . self::CONF_PATH_NAME . '.bak.php'); $result = file_put_contents(DATA_PATH . self::CONF_PATH_NAME, " Date: Sat, 6 Dec 2014 16:48:13 +0100 Subject: Fix typo (extensions) - change feedback.extension into feedback.extensions - disable button is pushed by default See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 20 ++++++++++---------- app/views/extension/index.phtml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index e348d9f31..543398b05 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -45,11 +45,11 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extension.not_found', $url_redirect); + Minz_Request::bad('feedback.extensions.not_found', $url_redirect); } if ($ext->is_enabled()) { - Minz_Request::bad('feedback.extension.already_enabled', $url_redirect); + Minz_Request::bad('feedback.extensions.already_enabled', $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -58,16 +58,16 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::addExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extension.enabled', $url_redirect); + Minz_Request::good('feedback.extensions.enabled', $url_redirect); } elseif ($ext->getType() === 'user') { $ext->install(); FreshRSS_Context::$conf->addExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.enabled', $url_redirect); + Minz_Request::good('feedback.extensions.enabled', $url_redirect); } else { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); + Minz_Request::bad('feedback.extensions.no_access', $url_redirect); } } @@ -90,11 +90,11 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extension.not_found', $url_redirect); + Minz_Request::bad('feedback.extensions.not_found', $url_redirect); } if (!$ext->is_enabled()) { - Minz_Request::bad('feedback.extension.not_enabled', $url_redirect); + Minz_Request::bad('feedback.extensions.not_enabled', $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -103,16 +103,16 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::removeExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extension.disabled', $url_redirect); + Minz_Request::good('feedback.extensions.disabled', $url_redirect); } elseif ($ext->getType() === 'user') { $ext->uninstall(); FreshRSS_Context::$conf->removeExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.disabled', $url_redirect); + Minz_Request::good('feedback.extensions.disabled', $url_redirect); } else { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); + Minz_Request::bad('feedback.extensions.no_access', $url_redirect); } } diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index 0be03d7b5..142ee4bc2 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -15,7 +15,7 @@
    is_enabled()) { ?> - + -- cgit v1.2.3 From 2da7c05fa6768b95a5cd0bd1c8f9934bbff05a03 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 17:15:20 +0100 Subject: Update i18n (extensions) See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 30 ++++++++++++++++++++---------- app/i18n/en/admin.php | 5 +++++ app/i18n/en/feedback.php | 8 ++++++++ app/i18n/en/gen.php | 7 +++++++ app/i18n/fr/admin.php | 5 +++++ app/i18n/fr/feedback.php | 8 ++++++++ app/i18n/fr/gen.php | 7 +++++++ app/views/extension/index.phtml | 8 ++++---- 8 files changed, 64 insertions(+), 14 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 543398b05..35b001094 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -45,11 +45,13 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extensions.not_found', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); } if ($ext->is_enabled()) { - Minz_Request::bad('feedback.extensions.already_enabled', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name), + $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -58,16 +60,19 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::addExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extensions.enabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.enabled', $ext_name), + $url_redirect); } elseif ($ext->getType() === 'user') { $ext->install(); FreshRSS_Context::$conf->addExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extensions.enabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.enabled', $ext_name), + $url_redirect); } else { - Minz_Request::bad('feedback.extensions.no_access', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); } } @@ -90,11 +95,13 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extensions.not_found', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); } if (!$ext->is_enabled()) { - Minz_Request::bad('feedback.extensions.not_enabled', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name), + $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -103,16 +110,19 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::removeExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extensions.disabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.disabled', $ext_name), + $url_redirect); } elseif ($ext->getType() === 'user') { $ext->uninstall(); FreshRSS_Context::$conf->removeExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extensions.disabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.disabled', $ext_name), + $url_redirect); } else { - Minz_Request::bad('feedback.extensions.no_access', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); } } diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index 74f01ae06..d73775d96 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -86,6 +86,11 @@ return array( 'ok' => 'You have ZIP extension.', ), ), + 'extensions' => array( + 'empty_list' => 'There is no installed extension', + 'system' => 'System extension (you have no rights on it)', + 'title' => 'Extensions', + ), 'users' => array( 'articles_and_size' => '%s articles (%s)', ), diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php index b3866f1dc..df1dc5725 100644 --- a/app/i18n/en/feedback.php +++ b/app/i18n/en/feedback.php @@ -1,6 +1,14 @@ array( + 'already_enabled' => '%s is already enabled', + 'disabled' => '%s is now disabled', + 'enabled' => '%s is now enabled', + 'no_access' => 'You have no access on %s', + 'not_enabled' => '%s is not enabled yet', + 'not_found' => '%s does not exist', + ), 'login' => array( 'error' => 'Login is invalid', 'success' => 'You are connected', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 9e06357bc..ba5f0c86d 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -190,10 +190,17 @@ return array( 'freshrss_installation' => 'Installation · FreshRSS', 'fri' => 'Fri', 'g+' => 'Google+', + 'actions' => array( + 'disable' => 'Disable', + 'enable' => 'Enable', + 'manage' => 'Manage', + 'remove' => 'Remove', + ), 'menu' => array( 'admin' => 'Administration', 'authentication' => 'Authentication', 'check_install' => 'Installation checking', + 'extensions' => 'Extensions', 'user_management' => 'Manage users', 'user_profile' => 'Profile', ), diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index ad1fae6c0..e46a5a7b0 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -86,6 +86,11 @@ return array( 'ok' => 'Vous disposez de l\'extension ZIP.', ), ), + 'extensions' => array( + 'empty_list' => 'Il n’y a aucune extension installée.', + 'system' => 'Extension système (vous n’avez aucun droit dessus)', + 'title' => 'Extensions', + ), 'users' => array( 'articles_and_size' => '%s articles (%s)', ), diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php index f4bb7cccf..7d3ab9db7 100644 --- a/app/i18n/fr/feedback.php +++ b/app/i18n/fr/feedback.php @@ -1,6 +1,14 @@ array( + 'already_enabled' => '%s est déjà activée', + 'disabled' => '%s est désormais désactivée', + 'enabled' => '%s est désormais activée', + 'no_access' => 'Vous n’avez aucun accès sur %s', + 'not_enabled' => '%s n’est pas encore activée', + 'not_found' => '%s n’existe pas', + ), 'login' => array( 'error' => 'L’identifiant est invalide !', 'success' => 'Vous êtes désormais connecté', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 0b2395bd0..44d8fb837 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -191,10 +191,17 @@ return array( 'freshrss_installation' => 'Installation · FreshRSS', 'fri' => 'ven.', 'g+' => 'Google+', + 'actions' => array( + 'disable' => 'Désactiver', + 'enable' => 'Activer', + 'manage' => 'Gérer', + 'remove' => 'Supprimer', + ), 'menu' => array( 'admin' => 'Administration', 'authentication' => 'Authentification', 'check_install' => 'Vérification de l’installation', + 'extensions' => 'Extensions', 'user_management' => 'Gestion des utilisateurs', 'user_profile' => 'Profil', ), diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index 142ee4bc2..d34a84452 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -13,14 +13,14 @@ getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> getName()); ?>
    - + is_enabled()) { ?> - + - + - +
    -- cgit v1.2.3 From bc81979a6b25554c4832d5ccb41b427023096463 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 17:25:01 +0100 Subject: Add default behaviour for configure / remove ext See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 7 ++++++- app/views/extension/configure.phtml | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/views/extension/configure.phtml diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 35b001094..a1e39af37 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -130,6 +130,11 @@ class FreshRSS_extension_Controller extends Minz_ActionController { } public function removeAction() { - + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $url_redirect = array('c' => 'extension', 'a' => 'index'); + Minz_Request::bad('not implemented yet!', $url_redirect); } } diff --git a/app/views/extension/configure.phtml b/app/views/extension/configure.phtml new file mode 100644 index 000000000..a79e9baac --- /dev/null +++ b/app/views/extension/configure.phtml @@ -0,0 +1,12 @@ +partial('aside_configure'); +} + +?> + +
    +

    Extension name

    + Not implemented yet! +
    \ No newline at end of file -- cgit v1.2.3 From 08546af75ff9a25eac3409649ea4660fe070720c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 18:48:00 +0100 Subject: Add a first draft for hooks - New Extension->registerHook($hook_name, $hook_function) method to register a new hook - Only one hook works for the moment: entry_before_insert - ExtensionManager::callHook will need to evolve based on future hooks See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 11 ++++++-- lib/Minz/Extension.php | 10 +++++++ lib/Minz/ExtensionManager.php | 55 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 9990a852c..dce79c57a 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -329,9 +329,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $id = min(time(), $entry_date) . uSecString(); } + $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(); - $values['id'] = $id; - $values['is_read'] = $is_read; $entryDAO->addEntry($values, $prepared_statement); } } diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 490a5c5cb..c93ba2520 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -153,4 +153,14 @@ class Minz_Extension { public function registerViews() { Minz_View::addBasePathname($this->path); } + + /** + * Register a new hook. + * + * @param $hook_name the hook name (must exist). + * @param $hook_function the function name to call (must be callable). + */ + public function registerHook($hook_name, $hook_function) { + Minz_ExtensionManager::addHook($hook_name, $hook_function, $this); + } } diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 46e421bac..7557e178f 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -2,6 +2,8 @@ /** * An extension manager to load extensions present in EXTENSIONS_PATH. + * + * @todo see coding style for methods!! */ class Minz_ExtensionManager { private static $ext_metaname = 'metadata.json'; @@ -11,6 +13,11 @@ class Minz_ExtensionManager { private static $ext_auto_enabled = array(); + private static $hook_list = array( + 'entry_before_insert' => array(), // function($entry) + ); + private static $ext_to_hooks = array(); + /** * Initialize the extension manager by loading extensions in EXTENSIONS_PATH. * @@ -131,6 +138,8 @@ class Minz_ExtensionManager { in_array($name, self::$ext_auto_enabled)) { self::enable($ext->getName()); } + + self::$ext_to_hooks[$name] = array(); } /** @@ -187,4 +196,50 @@ class Minz_ExtensionManager { return self::$ext_list[$ext_name]; } + + /** + * Add a hook function to a given hook. + * + * The hook name must be a valid one. For the valid list, see self::$hook_list + * array keys. + * + * @param $hook_name the hook name (must exist). + * @param $hook_function the function name to call (must be callable). + * @param $ext the extension which register the hook. + */ + public static function addHook($hook_name, $hook_function, $ext) { + if (isset(self::$hook_list[$hook_name]) && is_callable($hook_function)) { + self::$hook_list[$hook_name][] = $hook_function; + self::$ext_to_hooks[$ext->getName()][] = $hook_name; + } + } + + /** + * Call functions related to a given hook. + * + * The hook name must be a valid one. For the valid list, see self::$hook_list + * array keys. + * + * @param $hook_name the hook to call. + * @param additionnal parameters (for signature, please see self::$hook_list comments) + * @todo hook functions will have different signatures. So the $res = func($args); + * $args = $res; will not work for all of them in the future. We must + * find a better way to call hooks. + */ + public static function callHook($hook_name) { + $args = func_get_args(); + unset($args[0]); + + $result = $args; + foreach (self::$hook_list[$hook_name] as $function) { + $result = call_user_func_array($function, $args); + + if (is_null($result)) { + break; + } + + $args = $result; + } + return $result; + } } -- cgit v1.2.3 From 5932c3427b060d4f0aeab92d7ed17c8e8d4fd1d7 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 14:31:50 +0100 Subject: Add entry_before_display hook See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/views/index/normal.phtml | 5 +++++ app/views/index/reader.phtml | 11 +++++++---- lib/Minz/ExtensionManager.php | 6 ++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index 02d621bd0..9cbd367d0 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -35,6 +35,11 @@ if (!empty($this->entries)) {
    entries as $item) { + $item = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($item)) { + continue; + } + if ($display_today && $item->isDay(FreshRSS_Days::TODAY, $today)) { ?>
    entries)) { $content_width = FreshRSS_Context::$conf->content_width; ?> -
    - entries as $item) { ?> - -
    +
    entries as $item) { + $item = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($item)) { + continue; + } + ?>
    array(), // function($entry) + 'entry_before_display' => array(), // function($entry) -> Entry | null + 'entry_before_insert' => array(), // function($entry) -> Entry | null ); private static $ext_to_hooks = array(); @@ -230,7 +232,7 @@ class Minz_ExtensionManager { $args = func_get_args(); unset($args[0]); - $result = $args; + $result = $args[1]; foreach (self::$hook_list[$hook_name] as $function) { $result = call_user_func_array($function, $args); -- cgit v1.2.3 From 7ef4d6c033d6d12a644b6cf39940591901fdcb3b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 14:39:02 +0100 Subject: Fix entry_before_insert hook The hook must be called also in: - feedController->addAction() - importExportController->importJson() See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 13 ++++++++++--- app/Controllers/importExportController.php | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index dce79c57a..0a7edbee3 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -174,10 +174,17 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feedDAO->beginTransaction(); foreach ($entries as $entry) { // Entries are added without any verification. + $entry->_feed($feed->id()); + $entry->_id(min(time(), $entry->date(true)) . uSecString()); + $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(); - $values['id_feed'] = $feed->id(); - $values['id'] = min(time(), $entry->date(true)) . uSecString(); - $values['is_read'] = $is_read; $entryDAO->addEntry($values, $prepared_statement); } $feedDAO->updateLastUpdate($feed->id()); diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 4e2dbd157..c67b30431 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -385,6 +385,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $entry->_id(min(time(), $entry->date(true)) . uSecString()); $entry->_tags($tags); + $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(); $id = $this->entryDAO->addEntry($values, $prepared_statement); -- cgit v1.2.3 From ea849d7c68cc3de33825c1daafd06b9f8bbf747c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 15:03:42 +0100 Subject: Prepare better organization of view files for exts View files must be well-splitted to simplify work for extensions. See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/views/helpers/index/normal/entry_bottom.phtml | 84 ++++++++++++ app/views/helpers/index/normal/entry_header.phtml | 40 ++++++ app/views/index/index.phtml | 23 ---- app/views/index/normal.phtml | 157 +++------------------- 4 files changed, 144 insertions(+), 160 deletions(-) create mode 100644 app/views/helpers/index/normal/entry_bottom.phtml create mode 100644 app/views/helpers/index/normal/entry_header.phtml diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml new file mode 100644 index 000000000..704644f99 --- /dev/null +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -0,0 +1,84 @@ +sharing; + } + + $bottomline_read = FreshRSS_Context::$conf->bottomline_read; + $bottomline_favorite = FreshRSS_Context::$conf->bottomline_favorite; + $bottomline_sharing = FreshRSS_Context::$conf->bottomline_sharing && (count($sharing) > 0); + $bottomline_tags = FreshRSS_Context::$conf->bottomline_tags; + $bottomline_date = FreshRSS_Context::$conf->bottomline_date; + $bottomline_link = FreshRSS_Context::$conf->bottomline_link; +?>
    • 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isRead()) { + $arUrl['params']['is_read'] = 0; + } + ?>entry->isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isFavorite()) { + $arUrl['params']['is_favorite'] = 0; + } + ?>entry->isFavorite() ? 'starred' : 'non-starred'); ?>
    • +
    • entry->link()); + $title = urlencode($this->entry->title() . ' · ' . $feed->name()); + ?> + +
    • entry->tags() : null; + if (!empty($tags)) { + ?>
    • + +
    • entry->date(); ?>
    • +
    diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml new file mode 100644 index 000000000..b751b884f --- /dev/null +++ b/app/views/helpers/index/normal/entry_header.phtml @@ -0,0 +1,40 @@ +topline_read; + $topline_favorite = FreshRSS_Context::$conf->topline_favorite; + $topline_date = FreshRSS_Context::$conf->topline_date; + $topline_link = FreshRSS_Context::$conf->topline_link; +?>
    • 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isRead()) { + $arUrl['params']['is_read'] = 0; + } + ?>entry->isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isFavorite()) { + $arUrl['params']['is_favorite'] = 0; + } + ?>entry->isFavorite() ? 'starred' : 'non-starred'); ?>
    • categories, $this->entry->feed()); //We most likely already have the feed object in cache + if ($feed == null) { + $feed = $this->entry->feed(true); + if ($feed == null) { + $feed = FreshRSS_Feed::example(); + } + } + ?>
    • ✇ name(); ?>
    • +
    • entry->title(); ?>
    • +
    • entry->date(); ?> 
    • + +
    diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml index 8b93461dd..e69de29bb 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -1,23 +0,0 @@ -renderHelper('view/normal_view'); - } elseif ($output === 'reader') { - $this->renderHelper('view/reader_view'); - } elseif ($output === 'rss') { - $this->renderHelper('view/rss_view'); - } else { - Minz_Request::_param('output', 'normal'); - $output = 'normal'; - $this->renderHelper('view/normal_view'); - } -} elseif ($output === 'rss') { - // token has already been checked in the controller so we can show the view - $this->renderHelper('view/rss_view'); -} else { - // Normally, it should not happen, but log it anyway - Minz_Log::error('Something is wrong in ' . __FILE__ . ' line ' . __LINE__); -} diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index 9cbd367d0..fd2289365 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -7,23 +7,8 @@ if (!empty($this->entries)) { $display_today = true; $display_yesterday = true; $display_others = true; - if (FreshRSS_Auth::hasAccess()) { - $sharing = FreshRSS_Context::$conf->sharing; - } else { - $sharing = array(); - } $hidePosts = !FreshRSS_Context::$conf->display_posts; $lazyload = FreshRSS_Context::$conf->lazyload; - $topline_read = FreshRSS_Context::$conf->topline_read; - $topline_favorite = FreshRSS_Context::$conf->topline_favorite; - $topline_date = FreshRSS_Context::$conf->topline_date; - $topline_link = FreshRSS_Context::$conf->topline_link; - $bottomline_read = FreshRSS_Context::$conf->bottomline_read; - $bottomline_favorite = FreshRSS_Context::$conf->bottomline_favorite; - $bottomline_sharing = FreshRSS_Context::$conf->bottomline_sharing && (count($sharing)); - $bottomline_tags = FreshRSS_Context::$conf->bottomline_tags; - $bottomline_date = FreshRSS_Context::$conf->bottomline_date; - $bottomline_link = FreshRSS_Context::$conf->bottomline_link; $content_width = FreshRSS_Context::$conf->content_width; @@ -35,12 +20,12 @@ if (!empty($this->entries)) {
    entries as $item) { - $item = Minz_ExtensionManager::callHook('entry_before_display', $item); - if (is_null($item)) { + $this->entry = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($this->entry)) { continue; } - if ($display_today && $item->isDay(FreshRSS_Days::TODAY, $today)) { + if ($display_today && $this->entry->isDay(FreshRSS_Days::TODAY, $today)) { ?>
    entries)) { ?>
    isDay(FreshRSS_Days::YESTERDAY, $today)) { + if ($display_yesterday && $this->entry->isDay(FreshRSS_Days::YESTERDAY, $today)) { ?>
    entries)) { ?>
    isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) { + if ($display_others && $this->entry->isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) { ?>
    -
    • 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?>isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?>isFavorite() ? 'starred' : 'non-starred'); ?>
    • categories, $item->feed()); //We most likely already have the feed object in cache - if ($feed == null) { - $feed = $item->feed(true); - if ($feed == null) { - $feed = FreshRSS_Feed::example(); - } - } - ?>
    • ✇ name(); ?>
    • -
    • title(); ?>
    • -
    • date(); ?> 
    • - -
    + ?>
    renderHelper('index/normal/entry_header'); -
    + ?>
    -

    title(); ?>

    +

    entry->title(); ?>

    author(); + $author = $this->entry->author(); echo $author != '' ? '
    ' . _t('by_author', $author) . '
    ' : '', - $lazyload && $hidePosts ? lazyimg($item->content()) : $item->content(); + $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content(); ?> -
    -
    • 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?>isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?>isFavorite() ? 'starred' : 'non-starred'); ?>
    • -
    • link()); - $title = urlencode($item->title() . ' · ' . $feed->name()); - ?> -
    • - - - -
    -
    - - tags() : null; - if (!empty($tags)) { - ?>
  • - -
  • date(); ?>
  • - -
    -
    - + $this->renderHelper('index/normal/entry_bottom'); - renderHelper('pagination'); ?> -
    + ?>
    +
    renderHelper('pagination'); +?>
    partial('nav_entries'); ?> -- cgit v1.2.3 From 198b154064a12cfbeefd494afaa69378e77ffce9 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 15:30:24 +0100 Subject: Fix View files inclusion. Now, each part of the view (layout, partials, helpers, views) is included based on the $base_pathnames attribute. Extensions can now override all of these files. See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/View.php | 62 ++++++++++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/lib/Minz/View.php b/lib/Minz/View.php index 1bc2e862d..44034ae9a 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -13,7 +13,7 @@ class Minz_View { const LAYOUT_FILENAME = '/layout.phtml'; private $view_filename = ''; - private $use_layout = null; + private $use_layout = true; private static $base_pathnames = array(APP_PATH); private static $title = ''; @@ -56,9 +56,6 @@ class Minz_View { * Construit la vue */ public function build () { - if ($this->use_layout === null) { //TODO: avoid file_exists and require views to be explicit - $this->use_layout = file_exists (APP_PATH . self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME); - } if ($this->use_layout) { $this->buildLayout (); } else { @@ -66,35 +63,40 @@ class Minz_View { } } + /** + * Include a view file. + * + * The file is searched inside list of $base_pathnames. + * + * @param $filename the name of the file to include. + * @return true if the file has been included, false else. + */ + private function includeFile($filename) { + // We search the filename in the list of base pathnames. Only the first view + // found is considered. + foreach (self::$base_pathnames as $base) { + $absolute_filename = $base . $filename; + if (file_exists($absolute_filename)) { + include $absolute_filename; + return true; + } + } + + return false; + } + /** * Construit le layout */ public function buildLayout () { - include ( - APP_PATH - . self::LAYOUT_PATH_NAME - . self::LAYOUT_FILENAME - ); + $this->includeFile(self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME); } /** * Affiche la Vue en elle-même */ public function render () { - $view_found = false; - - // We search the view in the list of base pathnames. Only the first view - // found is considered. - foreach (self::$base_pathnames as $base) { - $filename = $base . $this->view_filename; - if (file_exists($filename)) { - include $filename; - $view_found = true; - break; - } - } - - if (!$view_found) { + if (!$this->includeFile($this->view_filename)) { Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } @@ -104,11 +106,8 @@ class Minz_View { * @param $part l'élément partial à ajouter */ public function partial ($part) { - $fic_partial = APP_PATH - . self::LAYOUT_PATH_NAME . '/' - . $part . '.phtml'; - - if ((include($fic_partial)) === false) { + $fic_partial = self::LAYOUT_PATH_NAME . '/' . $part . '.phtml'; + if (!$this->includeFile($fic_partial)) { Minz_Log::warning('File not found: `' . $fic_partial . '`'); } } @@ -118,11 +117,8 @@ class Minz_View { * @param $helper l'élément à afficher */ public function renderHelper ($helper) { - $fic_helper = APP_PATH - . '/views/helpers/' - . $helper . '.phtml'; - - if ((include($fic_helper)) === false) {; + $fic_helper = '/views/helpers/' . $helper . '.phtml'; + if (!$this->includeFile($fic_helper)) { Minz_Log::warning('File not found: `' . $fic_helper . '`'); } } -- cgit v1.2.3 From 2232d1e02a4bc9dbaa99cdbd22efad116ec01403 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 15:45:34 +0100 Subject: Load user extensions after all the global inits See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/FreshRSS.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/FreshRSS.php b/app/FreshRSS.php index b91dfcc46..88fe60850 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -29,18 +29,18 @@ class FreshRSS extends Minz_FrontController { // Load context and configuration. FreshRSS_Context::init(); - // Enable extensions for the current (logged) user. - if (FreshRSS_Auth::hasAccess()) { - $ext_list = FreshRSS_Context::$conf->extensions_enabled; - Minz_ExtensionManager::enable_by_list($ext_list); - } - // Init i18n. Minz_Session::_param('language', FreshRSS_Context::$conf->language); Minz_Translate::init(); $this->loadStylesAndScripts(); $this->loadNotifications(); + + // Enable extensions for the current (logged) user. + if (FreshRSS_Auth::hasAccess()) { + $ext_list = FreshRSS_Context::$conf->extensions_enabled; + Minz_ExtensionManager::enable_by_list($ext_list); + } } private function loadStylesAndScripts() { -- cgit v1.2.3 From c6dfec3ad351ee3b828c6a2c0a273bad5d9ac0df Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 12:01:47 +0100 Subject: Add behaviour to configure action (extensions) - Put extension configure view in dir_ext/configure.phtml - Handle POST action in Extension->handleConfigureAction() method See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 16 ++++++++++++++++ app/views/extension/configure.phtml | 17 ++++++++++++++--- lib/Minz/Extension.php | 21 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index a1e39af37..73b8070cb 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -27,6 +27,22 @@ class FreshRSS_extension_Controller extends Minz_ActionController { if (Minz_Request::param('ajax')) { $this->view->_useLayout(false); } + + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Error::error(404); + } + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $this->view->extension = $ext; + + if (Minz_Request::isPost()) { + $this->view->extension->handleConfigureAction(); + } } /** diff --git a/app/views/extension/configure.phtml b/app/views/extension/configure.phtml index a79e9baac..295080d5e 100644 --- a/app/views/extension/configure.phtml +++ b/app/views/extension/configure.phtml @@ -7,6 +7,17 @@ if (!Minz_Request::param('ajax')) { ?>
    -

    Extension name

    - Not implemented yet! -
    \ No newline at end of file +

    extension->getName(); ?> (extension->getVersion(); ?>) — extension->getType(); ?>

    + +

    extension->getDescription(); ?> — extension->getAuthor(); ?>

    + +

    + extension->getConfigureView(); + if ($configure_view !== false) { + echo $configure_view; + } else { + ?> +

    + +
    diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index c93ba2520..1d706ed80 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -86,6 +86,27 @@ class Minz_Extension { return $this->is_enabled; } + /** + * Return the content of the configure view for the current extension. + * + * @return the html content from ext_dir/configure.phtml, false if it does + * not exist. + */ + public function getConfigureView() { + $filename = $this->path . '/configure.phtml'; + if (!file_exists($filename)) { + return false; + } + return @file_get_contents($filename); + } + + /** + * Handle the configure POST action. + * + * It must be redefined by child classes. + */ + public function handleConfigureAction() {} + /** * Getters and setters. */ -- cgit v1.2.3 From a596385343a0307bb81d1662f78106d8f7e2dbfb Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 12:37:06 +0100 Subject: Fix a security issue in Minz_Error::error() Mehtod must redirect automatically by default to avoid code execution after calling the method. --- lib/Minz/Error.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Minz/Error.php b/lib/Minz/Error.php index c8222a430..e5f3dff07 100644 --- a/lib/Minz/Error.php +++ b/lib/Minz/Error.php @@ -19,7 +19,7 @@ class Minz_Error { * > $logs['notice'] * @param $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis) */ - public static function error ($code = 404, $logs = array (), $redirect = false) { + public static function error ($code = 404, $logs = array (), $redirect = true) { $logs = self::processLogs ($logs); $error_filename = APP_PATH . '/Controllers/errorController.php'; -- cgit v1.2.3 From 188b517daa174ce494f31dec02ae2cff122488ff Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:05:56 +0100 Subject: Add a feed_before_insert hook See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 6 ++++++ app/Controllers/importExportController.php | 30 ++++++++++++++++++++---------- lib/Minz/ExtensionManager.php | 1 + 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 0a7edbee3..7dda3840e 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -138,6 +138,12 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->_category($cat); $feed->_httpAuth($http_auth); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (is_null($feed)) { + Minz_Request::bad(_t('feed_not_added', $feed->name()), $url_redirect); + } + $values = array( 'url' => $feed->url(), 'category' => $feed->category(), diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index c67b30431..52df5bf8b 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -259,10 +259,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed->_website($website); $feed->_description($description); - // addFeedObject checks if feed is already in DB so nothing else to - // check here - $id = $this->feedDAO->addFeedObject($feed); - $error = ($id === false); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } else { + $error = true; + } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); $error = true; @@ -427,13 +433,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed->_name($name); $feed->_website($website); - // addFeedObject checks if feed is already in DB so nothing else to - // check here. - $id = $this->feedDAO->addFeedObject($feed); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here. + $id = $this->feedDAO->addFeedObject($feed); - if ($id !== false) { - $feed->_id($id); - $return = $feed; + if ($id !== false) { + $feed->_id($id); + $return = $feed; + } } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 5491c7cf6..9e6a3155a 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -17,6 +17,7 @@ class Minz_ExtensionManager { private static $hook_list = array( 'entry_before_display' => array(), // function($entry) -> Entry | null 'entry_before_insert' => array(), // function($entry) -> Entry | null + 'feed_before_insert' => array(), // function($feed) -> Feed | null ); private static $ext_to_hooks = array(); -- cgit v1.2.3 From 28c77f22900a10ac43c6d61b2b2d1a146feac42a Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:12:12 +0100 Subject: Fix a bug with feed_before_insert hook $feed->name() was called on a null value. See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 7dda3840e..379b8d8da 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -139,9 +139,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->_httpAuth($http_auth); // Call the extension hook + $name = $feed->name(); $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); if (is_null($feed)) { - Minz_Request::bad(_t('feed_not_added', $feed->name()), $url_redirect); + Minz_Request::bad(_t('feed_not_added', $name), $url_redirect); } $values = array( -- cgit v1.2.3 From 0543f96a97fa860fdec38d61b117e6b5addf94b6 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:22:11 +0100 Subject: Update comments for ExtensionController See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 73b8070cb..cd56de9eb 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -23,6 +23,16 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $this->view->extension_list = Minz_ExtensionManager::list_extensions(); } + /** + * This action handles configuration of a given extension. + * + * Only administrator can configure a system extension. + * + * Parameters are: + * - e: the extension name (urlencoded) + * - additional parameters which should be handle by the extension + * handleConfigureAction() method (POST request). + */ public function configureAction() { if (Minz_Request::param('ajax')) { $this->view->_useLayout(false); @@ -49,6 +59,7 @@ class FreshRSS_extension_Controller extends Minz_ActionController { * This action enables a disabled extension for the current user. * * System extensions can only be enabled by an administrator. + * This action must be reached by a POST request. * * Parameter is: * - e: the extension name (urlencoded). @@ -99,6 +110,7 @@ class FreshRSS_extension_Controller extends Minz_ActionController { * This action disables an enabled extension for the current user. * * System extensions can only be disabled by an administrator. + * This action must be reached by a POST request. * * Parameter is: * - e: the extension name (urlencoded). @@ -145,6 +157,15 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Request::forward($url_redirect, true); } + /** + * This action handles deletion of an extension. + * + * Only administrator can remove an extension. + * This action must be reached by a POST request. + * + * Parameter is: + * -e: extension name (urlencoded) + */ public function removeAction() { if (!FreshRSS_Auth::hasAccess('admin')) { Minz_Error::error(403); -- cgit v1.2.3 From 76358846abe2eba95668d66d3847cbdfe3f8bcdc Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:36:08 +0100 Subject: Implement extension deletion See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 22 +++++++++++++++++++++- lib/lib_rss.php | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index cd56de9eb..adb3e1864 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -172,6 +172,26 @@ class FreshRSS_extension_Controller extends Minz_ActionController { } $url_redirect = array('c' => 'extension', 'a' => 'index'); - Minz_Request::bad('not implemented yet!', $url_redirect); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + $res = recursive_unlink($ext->getPath()); + if ($res) { + Minz_Request::good(_t('feedback.extensions.removed', $ext_name), + $url_redirect); + } else { + Minz_Request::bad(_t('feedback.extensions.cannot_delete', $ext_name), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); } } diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 8170c7fd9..e466bcb15 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -319,3 +319,29 @@ function check_install_database() { return $status; } + +/** + * Remove a directory recursively. + * + * From http://php.net/rmdir#110489 + * + * @param $dir the directory to remove + */ +function recursive_unlink($dir) { + if (!is_dir($dir)) { + return true; + } + + $files = array_diff(scandir($dir), array('.', '..')); + foreach ($files as $filename) { + $filename = $dir . '/' . $filename; + if (is_dir($filename)) { + @chmod($filename, 0777); + recursive_unlink($filename); + } else { + unlink($filename); + } + } + + return rmdir($dir); +} -- cgit v1.2.3 From 70c1f0ebb08e2edf1721e2d3f8f98837c5910da4 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Tue, 9 Dec 2014 12:43:33 +0100 Subject: Fix redirection after feed refresh Fix https://github.com/FreshRSS/FreshRSS/issues/716 --- app/Controllers/feedController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 9990a852c..c8727c727 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -392,8 +392,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Redirect to the main page with correct notification. if ($updated_feeds === 1) { $feed = reset($feeds); - Minz_Request::good(_t('feed_actualized', $feed->name()), - array('get' => 'f_' . $feed->id())); + Minz_Request::good(_t('feed_actualized', $feed->name()), array( + 'params' => array('get' => 'f_' . $feed->id()) + )); } elseif ($updated_feeds > 1) { Minz_Request::good(_t('n_feeds_actualized', $updated_feeds), array()); } else { -- cgit v1.2.3 From 0b9fa9896ed3b12b7e0d6300f0122d25d9576aa3 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Tue, 9 Dec 2014 15:44:13 +0100 Subject: Update CHANGELOG --- CHANGELOG | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e09b55d21..15582e82a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,24 @@ # Journal des modifications -## Dev +## 2014-12-xx FreshRSS 0.9.3 (beta) * SimplePie * Support for content-type application/x-rss+xml * New force_feed option (for feeds sent with the wrong content-type / MIME) by adding #force_feed at the end of the feed URL * Improved error messages +* Statistics + * Add information on feed repartition pages + * Add percent repartition for the bigger feeds +* UI + * New theme selector + * Update Screwdriver theme + * Add BlueLagoon theme by Mister aiR +* Misc. + * Add option to remove articles after reading them + * Add comments + * Refactor i18n system to not load unnecessary strings + * Fix security issue in Minz_Error::error() method + * Fix redirection after refreshing a given feed ## 2014-10-31 FreshRSS 0.9.2 (beta) -- cgit v1.2.3 From f807a6f5c1e9aa5aadf338c5242e54e1930b4088 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 10 Dec 2014 22:10:01 +0100 Subject: Fix i18n for header and aside_configure --- app/i18n/en/gen.php | 58 +++++++++++++++++++++------------------- app/i18n/fr/gen.php | 58 +++++++++++++++++++++------------------- app/layout/aside_configure.phtml | 16 +++++------ app/layout/header.phtml | 32 +++++++++++----------- 4 files changed, 84 insertions(+), 80 deletions(-) diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index a7f51e8f9..bd386f6a0 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -1,6 +1,36 @@ array( + 'login' => 'Login', + 'logout' => 'Logout', + ), + 'menu' => array( + 'about' => 'About', + 'admin' => 'Administration', + 'archiving' => 'Archiving', + 'authentication' => 'Authentication', + 'check_install' => 'Installation checking', + 'configuration' => 'Configuration', + 'display' => 'Display', + 'logs' => 'Logs', + 'queries' => 'User queries', + 'reading' => 'Reading', + 'sharing' => 'Sharing', + 'shortcuts' => 'Shortcuts', + 'stats' => 'Statistics', + 'update' => 'Update', + 'user_management' => 'Manage users', + 'user_profile' => 'Profile', + ), + 'title' => array( + '_' => 'Title', + 'authentication' => 'Authentication', + 'check_install' => 'Installation checking', + 'global_view' => 'Global view', + 'user_management' => 'Manage users', + 'user_profile' => 'Profile', + ), 'Apr' => '\\A\\p\\r\\i\\l', 'Aug' => '\\A\\u\\g\\u\\s\\t', 'Dec' => '\\D\\e\\c\\e\\m\\b\\e\\r', @@ -13,7 +43,6 @@ return array( 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r', 'Oct' => '\\O\\c\\t\\o\\b\\e\\r', 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r', - 'about' => 'About', 'about_freshrss' => 'About FreshRSS', 'access_denied' => 'You don’t have permission to access this page', 'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds', @@ -33,7 +62,6 @@ return array( 'api_enabled' => 'Allow API access (required for mobile apps)', 'apr' => 'apr', 'april' => 'Apr', - 'archiving_configuration' => 'Archiving', 'archiving_configuration_help' => 'More options are available in the individual stream settings', 'article' => 'Article', 'article_icons' => 'Article icons', @@ -110,7 +138,6 @@ return array( 'choose_language' => 'Choose a language for FreshRSS', 'clear_logs' => 'Clear the logs', 'collapse_article' => 'Collapse', - 'configuration' => 'Configuration', 'configuration_updated' => 'Configuration has been updated', 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!', @@ -136,7 +163,6 @@ return array( 'delete' => 'Delete', 'delete_articles_every' => 'Remove articles after', 'diaspora' => 'Diaspora*', - 'display' => 'Display', 'display_articles_unfolded' => 'Show articles unfolded by default', 'display_categories_unfolded' => 'Show categories folded by default', 'display_configuration' => 'Display', @@ -191,21 +217,6 @@ return array( 'freshrss_installation' => 'Installation · FreshRSS', 'fri' => 'Fri', 'g+' => 'Google+', - 'menu' => array( - 'admin' => 'Administration', - 'authentication' => 'Authentication', - 'check_install' => 'Installation checking', - 'user_management' => 'Manage users', - 'user_profile' => 'Profile', - ), - 'title' => array( - '_' => 'Title', - 'authentication' => 'Authentication', - 'check_install' => 'Installation checking', - 'global_view' => 'Global view', - 'user_management' => 'Manage users', - 'user_profile' => 'Profile', - ), 'general_conf_is_ok' => 'General configuration has been saved.', 'general_configuration' => 'General configuration', 'github_or_email' => 'on Github or by mail', @@ -256,13 +267,10 @@ return array( 'license' => 'License', 'load_more' => 'Load more articles', 'log_is_ok' => 'Permissions on logs directory are good', - 'login' => 'Login', 'login_configuration' => 'Login', 'login_persona_problem' => 'Connection problem with Persona?', 'login_required' => 'Login required:', 'login_with_persona' => 'Login with Persona', - 'logout' => 'Logout', - 'logs' => 'Logs', 'logs_empty' => 'Log file is empty', 'main_stream' => 'Main stream', 'mar' => 'mar', @@ -340,7 +348,6 @@ return array( 'publication_date' => 'Date of publication', 'purge_completed' => 'Purge completed (%d articles deleted)', 'purge_now' => 'Purge now', - 'queries' => 'User queries', 'query_created' => 'Query "%s" has been created.', 'query_deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.', 'query_filter' => 'Filter applied:', @@ -370,7 +377,6 @@ return array( 'query_state_15' => 'Display all articles', 'random_string' => 'Random string', 'reader_view' => 'Reading view', - 'reading_configuration' => 'Reading', 'reading_confirm' => 'Display a confirmation dialog on “mark all as read” actions', 'refresh' => 'Refresh', 'related_tags' => 'Related tags', @@ -392,10 +398,8 @@ return array( 'share' => 'Share', 'share_name' => 'Share name to display', 'share_url' => 'Share URL to use', - 'sharing' => 'Sharing', 'sharing_management' => 'Sharing options management', 'shift_for_all_read' => '+ shift to mark all articles as read', - 'shortcuts' => 'Shortcuts', 'shortcuts_article_action' => 'Article actions', 'shortcuts_navigation' => 'Navigation', 'shortcuts_navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.
    With the "Alt" modifier, navigation shortcuts apply on categories.', @@ -410,7 +414,6 @@ return array( 'show_read' => 'Show only read', 'sort_order' => 'Sort order', 'starred_list' => 'List of favourite articles', - 'stats' => 'Statistics', 'stats_entry_count' => 'Entry count', 'stats_entry_per_category' => 'Entries per category', 'stats_entry_per_day' => 'Entries per day (last 30 days)', @@ -445,7 +448,6 @@ return array( 'tue' => 'Tue', 'twitter' => 'Twitter', 'unsafe_autologin' => 'Allow unsafe automatic login using the format: ', - 'update' => 'Update', 'update_apply' => 'Apply', 'update_can_apply' => 'An update is available.', 'update_check' => 'Check for new updates', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 0b2395bd0..b33dc49bc 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -1,6 +1,36 @@ array( + 'login' => 'Connexion', + 'logout' => 'Déconnexion', + ), + 'menu' => array( + 'about' => 'À propos', + 'admin' => 'Administration', + 'archiving' => 'Archivage', + 'authentication' => 'Authentification', + 'check_install' => 'Vérification de l’installation', + 'configuration' => 'Configuration', + 'display' => 'Affichage', + 'logs' => 'Logs', + 'queries' => 'Filtres utilisateurs', + 'reading' => 'Lecture', + 'sharing' => 'Partage', + 'shortcuts' => 'Raccourcis', + 'stats' => 'Statistiques', + 'update' => 'Mise à jour', + 'user_management' => 'Gestion des utilisateurs', + 'user_profile' => 'Profil', + ), + 'title' => array( + '_' => 'Titre', + 'authentication' => 'Authentification', + 'check_install' => 'Vérification de l’installation', + 'global_view' => 'Vue globale', + 'user_management' => 'Gestion des utilisateurs', + 'user_profile' => 'Profil', + ), 'Apr' => '\\a\\v\\r\\i\\l', 'Aug' => '\\a\\o\\û\\t', 'Dec' => '\\d\\é\\c\\e\\m\\b\\r\\e', @@ -13,7 +43,6 @@ return array( 'Nov' => '\\n\\o\\v\\e\\m\\b\\r\\e', 'Oct' => '\\o\\c\\t\\o\\b\\r\\e', 'Sep' => '\\s\\e\\p\\t\\e\\m\\b\\r\\e', - 'about' => 'À propos', 'about_freshrss' => 'À propos de FreshRSS', 'access_denied' => 'Vous n’avez pas le droit d’accéder à cette page !', 'access_protected_feeds' => 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.', @@ -33,7 +62,6 @@ return array( 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applis mobiles)', 'apr' => 'avr.', 'april' => 'avril', - 'archiving_configuration' => 'Archivage', 'archiving_configuration_help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.', 'article' => 'Article', 'article_icons' => 'Icônes d’article', @@ -110,7 +138,6 @@ return array( 'choose_language' => 'Choisissez la langue pour FreshRSS', 'clear_logs' => 'Effacer les logs', 'collapse_article' => 'Refermer', - 'configuration' => 'Configuration', 'configuration_updated' => 'La configuration a été mise à jour.', 'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !', 'confirm_action_feed_cat' => 'Êtes-vous sûr(e) de vouloir continuer ? Vous perdrez les favoris et les filtres associés. Cette action ne peut être annulée !', @@ -139,7 +166,6 @@ return array( 'display' => 'Affichage', 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', 'display_categories_unfolded' => 'Afficher les catégories pliées par défaut', - 'display_configuration' => 'Affichage', 'do_not_change_if_doubt' => 'Laissez tel quel dans le doute', 'dom_is_nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml)', 'dom_is_ok' => 'Vous disposez du nécessaire pour parcourir le DOM', @@ -191,21 +217,6 @@ return array( 'freshrss_installation' => 'Installation · FreshRSS', 'fri' => 'ven.', 'g+' => 'Google+', - 'menu' => array( - 'admin' => 'Administration', - 'authentication' => 'Authentification', - 'check_install' => 'Vérification de l’installation', - 'user_management' => 'Gestion des utilisateurs', - 'user_profile' => 'Profil', - ), - 'title' => array( - '_' => 'Titre', - 'authentication' => 'Authentification', - 'check_install' => 'Vérification de l’installation', - 'global_view' => 'Vue globale', - 'user_management' => 'Gestion des utilisateurs', - 'user_profile' => 'Profil', - ), 'general_conf_is_ok' => 'La configuration générale a été enregistrée.', 'general_configuration' => 'Configuration générale', 'github_or_email' => 'sur Github ou par courriel', @@ -256,13 +267,10 @@ return array( 'license' => 'Licence', 'load_more' => 'Charger plus d’articles', 'log_is_ok' => 'Les droits sur le répertoire des logs sont bons', - 'login' => 'Connexion', 'login_configuration' => 'Identification', 'login_persona_problem' => 'Problème de connexion à Persona ?', 'login_required' => 'Accès protégé par mot de passe :', 'login_with_persona' => 'Connexion avec Persona', - 'logout' => 'Déconnexion', - 'logs' => 'Logs', 'logs_empty' => 'Les logs sont vides.', 'main_stream' => 'Flux principal', 'mar' => 'mar.', @@ -340,7 +348,6 @@ return array( 'publication_date' => 'Date de publication', 'purge_completed' => 'Purge effectuée (%d articles supprimés).', 'purge_now' => 'Purger maintenant', - 'queries' => 'Filtres utilisateurs', 'query_created' => 'Le filtre "%s" a bien été créé.', 'query_deprecated' => 'Ce filtre n’est plus valide. La catégorie ou le flux concerné a été supprimé.', 'query_filter' => 'Filtres appliqués :', @@ -370,7 +377,6 @@ return array( 'query_state_15' => 'Afficher tous les articles', 'random_string' => 'Chaîne aléatoire', 'reader_view' => 'Vue lecture', - 'reading_configuration' => 'Lecture', 'reading_confirm' => 'Afficher une confirmation lors des actions “marquer tout comme lu”', 'refresh' => 'Actualisation', 'related_tags' => 'Tags associés', @@ -392,10 +398,8 @@ return array( 'share' => 'Partager', 'share_name' => 'Nom du partage à afficher', 'share_url' => 'URL du partage à utiliser', - 'sharing' => 'Partage', 'sharing_management' => 'Gestion des options de partage', 'shift_for_all_read' => '+ shift pour marquer tous les articles comme lus', - 'shortcuts' => 'Raccourcis', 'shortcuts_article_action' => 'Actions associées à l’article courant', 'shortcuts_navigation' => 'Navigation', 'shortcuts_navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.
    Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.', @@ -410,7 +414,6 @@ return array( 'show_read' => 'Afficher les lus', 'sort_order' => 'Ordre de tri', 'starred_list' => 'Liste des articles favoris', - 'stats' => 'Statistiques', 'stats_entry_count' => 'Nombre d’articles', 'stats_entry_per_category' => 'Articles par catégorie', 'stats_entry_per_day' => 'Nombre d’articles par jour (30 derniers jours)', @@ -445,7 +448,6 @@ return array( 'tue' => 'mar.', 'twitter' => 'Twitter', 'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ', - 'update' => 'Mise à jour', 'update_apply' => 'Appliquer la mise à jour', 'update_can_apply' => 'Une mise à jour est disponible.', 'update_check' => 'Vérifier les mises à jour', diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml index 53c52d3e3..25b8037e6 100644 --- a/app/layout/aside_configure.phtml +++ b/app/layout/aside_configure.phtml @@ -1,22 +1,22 @@ diff --git a/app/layout/header.phtml b/app/layout/header.phtml index c73d9cdbb..429cfc1d2 100644 --- a/app/layout/header.phtml +++ b/app/layout/header.phtml @@ -2,9 +2,9 @@ if (Minz_Configuration::canLogIn()) { ?>" method="get">
    - + @@ -55,13 +55,13 @@ if (Minz_Configuration::canLogIn()) {
    - +
    -- cgit v1.2.3 From 7dee863577aff2103277e08bf6db2f8e16b029a7 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 10 Dec 2014 22:29:13 +0100 Subject: Fix i18n for aside_feed --- app/i18n/en/gen.php | 14 ++++++-------- app/i18n/en/index.php | 13 ++++++++++++- app/i18n/fr/gen.php | 14 ++++++-------- app/i18n/fr/index.php | 13 ++++++++++++- app/layout/aside_feed.phtml | 20 ++++++++++---------- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index bd386f6a0..3bc2c5ac6 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -1,6 +1,12 @@ array( + 'disable' => 'Disable', + 'enable' => 'Enable', + 'manage' => 'Manage', + 'remove' => 'Remove', + ), 'auth' => array( 'login' => 'Login', 'logout' => 'Logout', @@ -43,11 +49,9 @@ return array( 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r', 'Oct' => '\\O\\c\\t\\o\\b\\e\\r', 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r', - 'about_freshrss' => 'About FreshRSS', 'access_denied' => 'You don’t have permission to access this page', 'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds', 'activate_sharing' => 'Activate sharing', - 'actualize' => 'Actualize', 'add_category' => 'Add a category', 'add_query' => 'Add a query', 'add_rss_feed' => 'Add a RSS feed', @@ -179,7 +183,6 @@ return array( 'export_starred' => 'Export your favourites', 'facebook' => 'Facebook', 'favicons_is_ok' => 'Permissions on favicons directory are good', - 'favorite_feeds' => 'Favourites (%s)', 'feb' => 'feb', 'february' => 'Feb', 'feed' => 'Feed', @@ -204,7 +207,6 @@ return array( 'file_is_nok' => 'Check permissions on %s directory. HTTP server must have rights to write into', 'file_to_import' => 'File to import
    (OPML, Json or Zip)', 'file_to_import_no_zip' => 'File to import
    (OPML or Json)', - 'filter' => 'Filter', 'finish_installation' => 'Complete installation', 'first' => 'First', 'first_article' => 'Skip to the first article', @@ -272,14 +274,12 @@ return array( 'login_required' => 'Login required:', 'login_with_persona' => 'Login with Persona', 'logs_empty' => 'Log file is empty', - 'main_stream' => 'Main stream', 'mar' => 'mar', 'march' => 'Mar', 'mark_all_read' => 'Mark all as read', 'mark_cat_read' => 'Mark category as read', 'mark_favorite' => 'Mark as favourite', 'mark_feed_read' => 'Mark feed as read', - 'mark_read' => 'Mark as read', 'may' => 'May', 'minz_is_nok' => 'You lack the Minz framework. You should execute build.sh script or download it on Github and install in %s directory the content of its /lib directory.', 'minz_is_ok' => 'You have the Minz framework', @@ -391,7 +391,6 @@ return array( 'search_short' => 'Search', 'seconds_(0_means_no_timeout)' => 'seconds (0 means no timeout)', 'see_on_website' => 'See on original website', - 'see_website' => 'See website', 'sep' => 'sep', 'september' => 'Sep', 'shaarli' => 'Shaarli', @@ -435,7 +434,6 @@ return array( 'steps' => 'Steps', 'sticky_post' => 'Stick the article to the top when opened', 'submit' => 'Submit', - 'subscription_management' => 'Subscriptions management', 'sun' => 'Sun', 'theme' => 'Theme', 'think_to_add' => 'You may add some feeds.', diff --git a/app/i18n/en/index.php b/app/i18n/en/index.php index afca37ed3..ca15bcc25 100644 --- a/app/i18n/en/index.php +++ b/app/i18n/en/index.php @@ -1,5 +1,16 @@ array( + 'about' => 'About FreshRSS', + 'actualize' => 'Actualize', + 'favorites' => 'Favourites (%s)', + 'filter' => 'Filter', + 'main_stream' => 'Main stream', + 'manage' => 'Manage', + 'mark_read' => 'Mark as read', + 'see_website' => 'See website', + 'stats' => 'Statistics', + 'subscription' => 'Subscriptions management', + ), ); diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index b33dc49bc..9d9624cc8 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -1,6 +1,12 @@ array( + 'disable' => 'Désactiver', + 'enable' => 'Activer', + 'manage' => 'Gérer', + 'remove' => 'Supprimer', + ), 'auth' => array( 'login' => 'Connexion', 'logout' => 'Déconnexion', @@ -43,11 +49,9 @@ return array( 'Nov' => '\\n\\o\\v\\e\\m\\b\\r\\e', 'Oct' => '\\o\\c\\t\\o\\b\\r\\e', 'Sep' => '\\s\\e\\p\\t\\e\\m\\b\\r\\e', - 'about_freshrss' => 'À propos de FreshRSS', 'access_denied' => 'Vous n’avez pas le droit d’accéder à cette page !', 'access_protected_feeds' => 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.', 'activate_sharing' => 'Activer le partage', - 'actualize' => 'Actualiser', 'add_category' => 'Ajouter une catégorie', 'add_query' => 'Créer un filtre', 'add_rss_feed' => 'Ajouter un flux RSS', @@ -179,7 +183,6 @@ return array( 'export_starred' => 'Exporter les favoris', 'facebook' => 'Facebook', 'favicons_is_ok' => 'Les droits sur le répertoire des favicons sont bons', - 'favorite_feeds' => 'Favoris (%s)', 'feb' => 'fév.', 'february' => 'février', 'feed' => 'Flux', @@ -204,7 +207,6 @@ return array( 'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans', 'file_to_import' => 'Fichier à importer
    (OPML, Json ou Zip)', 'file_to_import_no_zip' => 'Fichier à importer
    (OPML ou Json)', - 'filter' => 'Filtrer', 'finish_installation' => 'Terminer l’installation', 'first' => 'Début', 'first_article' => 'Passer au premier article', @@ -272,14 +274,12 @@ return array( 'login_required' => 'Accès protégé par mot de passe :', 'login_with_persona' => 'Connexion avec Persona', 'logs_empty' => 'Les logs sont vides.', - 'main_stream' => 'Flux principal', 'mar' => 'mar.', 'march' => 'mars', 'mark_all_read' => 'Tout marquer comme lu', 'mark_cat_read' => 'Marquer la catégorie comme lue', 'mark_favorite' => 'Mettre en favori', 'mark_feed_read' => 'Marquer le flux comme lu', - 'mark_read' => 'Marquer comme lu', 'may' => 'mai.', 'minz_is_nok' => 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script build.sh ou bien la télécharger sur Github et installer dans le répertoire %s le contenu de son répertoire /lib.', 'minz_is_ok' => 'Vous disposez du framework Minz', @@ -391,7 +391,6 @@ return array( 'search_short' => 'Rechercher', 'seconds_(0_means_no_timeout)' => 'secondes (0 signifie aucun timeout ) ', 'see_on_website' => 'Voir sur le site d’origine', - 'see_website' => 'Voir le site', 'sep' => 'sep.', 'september' => 'septembre', 'shaarli' => 'Shaarli', @@ -435,7 +434,6 @@ return array( 'steps' => 'Étapes', 'sticky_post' => 'Aligner l’article en haut quand il est ouvert', 'submit' => 'Valider', - 'subscription_management' => 'Gestion des abonnements', 'sun' => 'dim.', 'theme' => 'Thème', 'think_to_add' => 'Vous pouvez ajouter des flux.', diff --git a/app/i18n/fr/index.php b/app/i18n/fr/index.php index afca37ed3..408d7ee4a 100644 --- a/app/i18n/fr/index.php +++ b/app/i18n/fr/index.php @@ -1,5 +1,16 @@ array( + 'about' => 'À propos de FreshRSS', + 'actualize' => 'Actualiser', + 'favorites' => 'Favoris (%s)', + 'filter' => 'Filtrer', + 'main_stream' => 'Flux principal', + 'manage' => 'Gérer', + 'mark_read' => 'Marquer comme lu', + 'see_website' => 'Voir le site', + 'stats' => 'Statistiques', + 'subscription' => 'Gestion des abonnements', + ), ); diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index 3c23e0178..ca220dcd4 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -12,11 +12,11 @@
    - +
    - + @@ -24,13 +24,13 @@
    • - +
    • - +
    • @@ -74,21 +74,21 @@ "> - + diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml index 6577e0109..02f2014ee 100644 --- a/app/views/helpers/javascript_vars.phtml +++ b/app/views/helpers/javascript_vars.phtml @@ -1,6 +1,7 @@ "use strict"; mark_when; $mail = Minz_Session::param('mail', false); $auto_actualize = Minz_Session::param('actualize_feeds', false); @@ -29,7 +30,7 @@ echo 'var context={', 'does_lazyload:', FreshRSS_Context::$conf->lazyload ? 'true' : 'false', ',', 'sticky_post:', FreshRSS_Context::isStickyPostEnabled() ? 'true' : 'false', ',', 'html5_notif_timeout:', FreshRSS_Context::$conf->html5_notif_timeout, ',', - 'auth_type:"', Minz_Configuration::authType(), '",', + 'auth_type:"', $conf->general['auth_type'], '",', 'current_user_mail:', $mail ? ('"' . $mail . '"') : 'null', ',', 'current_view:"', Minz_Request::param('output', 'normal'), '"', "},\n"; diff --git a/data/config.default.php b/data/config.default.php new file mode 100644 index 000000000..a69d8050b --- /dev/null +++ b/data/config.default.php @@ -0,0 +1,32 @@ + array( + 'environment' => 'production', + 'salt' => '', + 'base_url' => '', + 'language' => 'en', + 'title' => 'FreshRSS', + 'default_user' => '_', + 'allow_anonymous' => false, + 'allow_anonymous_refresh' => false, + 'auth_type' => 'none', + 'api_enabled' => false, + 'unsafe_autologin_enabled' => false, + ), + 'limits' => array( + 'cache_duration' => 800, + 'timeout' => 10, + 'max_inactivity' => PHP_INT_MAX, + 'max_feeds' => 16384, + 'max_categories' => 16384, + ), + 'db' => array( + 'type' => 'sqlite', + 'host' => '', + 'user' => '', + 'password' => '', + 'base' => '', + 'prefix' => '', + ), +); diff --git a/data/users/_/config.default.php b/data/users/_/config.default.php new file mode 100644 index 000000000..56d54b293 --- /dev/null +++ b/data/users/_/config.default.php @@ -0,0 +1,66 @@ + 'en', + 'old_entries' => 3, + 'keep_history_default' => 0, + 'ttl_default' => 3600, + 'mail_login' => '', + 'token' => '', + 'passwordHash' => '', + 'apiPasswordHash' => '', + 'posts_per_page' => 20, + 'view_mode' => 'normal', + 'default_view' => 'adaptive', + 'default_state' => FreshRSS_Entry::STATE_NOT_READ, + 'auto_load_more' => true, + 'display_posts' => false, + 'display_categories' => false, + 'hide_read_feeds' => true, + 'onread_jump_next' => true, + 'lazyload' => true, + 'sticky_post' => true, + 'reading_confirm' => false, + 'auto_remove_article' => false, + 'sort_order' => 'DESC', + 'anon_access' => false, + 'mark_when' => array ( + 'article' => true, + 'site' => true, + 'scroll' => false, + 'reception' => false, + ), + 'theme' => 'Origine', + 'content_width' => 'thin', + 'shortcuts' => array ( + 'mark_read' => 'r', + 'mark_favorite' => 'f', + 'go_website' => 'space', + 'next_entry' => 'j', + 'prev_entry' => 'k', + 'first_entry' => 'home', + 'last_entry' => 'end', + 'collapse_entry' => 'c', + 'load_more' => 'm', + 'auto_share' => 's', + 'focus_search' => 'a', + 'user_filter' => 'u', + 'help' => 'f1', + 'close_dropdown' => 'escape', + ), + 'topline_read' => true, + 'topline_favorite' => true, + 'topline_date' => true, + 'topline_link' => true, + 'bottomline_read' => true, + 'bottomline_favorite' => true, + 'bottomline_sharing' => true, + 'bottomline_tags' => true, + 'bottomline_date' => true, + 'bottomline_link' => true, + 'sharing' => array ( + ), + 'queries' => array ( + ), + 'html5_notif_timeout' => 0, +); diff --git a/lib/Minz/BadConfigurationException.php b/lib/Minz/BadConfigurationException.php deleted file mode 100644 index a7b77d687..000000000 --- a/lib/Minz/BadConfigurationException.php +++ /dev/null @@ -1,9 +0,0 @@ - -*/ /** - * La classe Configuration permet de gérer la configuration de l'application + * Manage configuration for the application. */ class Minz_Configuration { - const CONF_PATH_NAME = '/config.php'; - /** - * VERSION est la version actuelle de MINZ + * The list of configurations. */ - const VERSION = '1.3.1.freshrss'; // version spéciale FreshRSS + private static $config_list = array(); /** - * valeurs possibles pour l'"environment" - * SILENT rend l'application muette (pas de log) - * PRODUCTION est recommandée pour une appli en production - * (log les erreurs critiques) - * DEVELOPMENT log toutes les erreurs + * Add a new configuration to the list of configuration. + * + * @param $namespace the name of the current configuration + * @param $config_filename the filename of the configuration + * @param $default_filename a filename containing default values for the configuration + * @throws Minz_ConfigurationNamespaceException if the namespace already exists. */ - const SILENT = 0; - const PRODUCTION = 1; - const DEVELOPMENT = 2; + public static function register($namespace, $config_filename, $default_filename = null) { + if (isset(self::$config_list[$namespace])) { + throw new Minz_ConfigurationNamespaceException( + $namespace . ' namespace already exists' + ); + } + + self::$config_list[$namespace] = new Minz_Configuration( + $namespace, $config_filename, $default_filename + ); + } /** - * définition des variables de configuration - * $salt une chaîne de caractères aléatoires (obligatoire) - * $environment gère le niveau d'affichage pour log et erreurs - * $base_url le chemin de base pour accéder à l'application - * $title le nom de l'application - * $language la langue par défaut de l'application - * $db paramètres pour la base de données (tableau) - * - host le serveur de la base - * - user nom d'utilisateur - * - password mot de passe de l'utilisateur - * - base le nom de la base de données + * Parse a file and return its data. + * + * If the file does not contain a valid PHP code returning an array, an + * empty array is returned anyway. + * + * @param $filename the name of the file to parse. + * @return an array of values + * @throws Minz_FileNotExistException if the file does not exist. */ - private static $salt = ''; - private static $environment = Minz_Configuration::PRODUCTION; - private static $base_url = ''; - private static $title = ''; - private static $language = 'en'; - private static $default_user = ''; - private static $allow_anonymous = false; - private static $allow_anonymous_refresh = false; - private static $auth_type = 'none'; - private static $api_enabled = false; - private static $unsafe_autologin_enabled = false; - - private static $db = array ( - 'type' => 'mysql', - 'host' => '', - 'user' => '', - 'password' => '', - 'base' => '', - 'prefix' => '', - ); - - const MAX_SMALL_INT = 16384; - private static $limits = array( - 'cache_duration' => 800, //SimplePie cache duration in seconds - 'timeout' => 10, //SimplePie timeout in seconds - 'max_inactivity' => PHP_INT_MAX, //Time in seconds after which a user who has not used the account is considered inactive (no auto-refresh of feeds). - 'max_feeds' => Minz_Configuration::MAX_SMALL_INT, - 'max_categories' => Minz_Configuration::MAX_SMALL_INT, - ); + public static function parseFile($filename) { + if (!file_exists($filename)) { + throw new Minz_FileNotExistException($filename); + } - /* - * Getteurs - */ - public static function salt () { - return self::$salt; + $data = @include($filename); + if (is_array($data)) { + return $data; + } else { + return array(); + } } - public static function environment ($str = false) { - $env = self::$environment; - if ($str) { - switch (self::$environment) { - case self::SILENT: - $env = 'silent'; - break; - case self::DEVELOPMENT: - $env = 'development'; - break; - case self::PRODUCTION: - default: - $env = 'production'; - } + /** + * Return the configuration related to a given namespace. + * + * @param $namespace the name of the configuration to get. + * @return a Minz_Configuration object + * @throws Minz_ConfigurationNamespaceException if the namespace does not exist. + */ + public static function get($namespace) { + if (!isset(self::$config_list[$namespace])) { + throw new Minz_ConfigurationNamespaceException( + $namespace . ' namespace does not exist' + ); } - return $env; - } - public static function baseUrl () { - return self::$base_url; - } - public static function title () { - return self::$title; - } - public static function language () { - return self::$language; - } - public static function dataBase () { - return self::$db; - } - public static function limits() { - return self::$limits; - } - public static function defaultUser () { - return self::$default_user; - } - public static function allowAnonymous() { - return self::$allow_anonymous; - } - public static function allowAnonymousRefresh() { - return self::$allow_anonymous_refresh; - } - public static function authType() { - return self::$auth_type; - } - public static function needsLogin() { - return self::$auth_type !== 'none'; - } - public static function canLogIn() { - return self::$auth_type === 'form' || self::$auth_type === 'persona'; - } - public static function apiEnabled() { - return self::$api_enabled; - } - public static function unsafeAutologinEnabled() { - return self::$unsafe_autologin_enabled; + return self::$config_list[$namespace]; } - public static function _allowAnonymous($allow = false) { - self::$allow_anonymous = ((bool)$allow) && self::canLogIn(); - } - public static function _allowAnonymousRefresh($allow = false) { - self::$allow_anonymous_refresh = ((bool)$allow) && self::allowAnonymous(); - } - public static function _authType($value) { - $value = strtolower($value); - switch ($value) { - case 'form': - case 'http_auth': - case 'persona': - case 'none': - self::$auth_type = $value; - break; - } - self::_allowAnonymous(self::$allow_anonymous); - } + /** + * The namespace of the current configuration. + */ + private $namespace = ''; - public static function _enableApi($value = false) { - self::$api_enabled = (bool)$value; - } - public static function _enableAutologin($value = false) { - self::$unsafe_autologin_enabled = (bool)$value; - } + /** + * The filename for the current configuration. + */ + private $config_filename = ''; + + /** + * The filename for the current default values, null by default. + */ + private $default_filename = null; + + /** + * The configuration values, an empty array by default. + */ + private $data = array(); + + /** + * The default values, an empty array by default. + */ + private $data_default = array(); /** - * Initialise les variables de configuration - * @exception Minz_FileNotExistException si le CONF_PATH_NAME n'existe pas - * @exception Minz_BadConfigurationException si CONF_PATH_NAME mal formaté + * Create a new Minz_Configuration object. + * + * @param $namespace the name of the current configuration. + * @param $config_filename the file containing configuration values. + * @param $default_filename the file containing default values, null by default. */ - public static function init () { + private function __construct($namespace, $config_filename, $default_filename = null) { + $this->namespace = $namespace; + $this->config_filename = $config_filename; + try { - self::parseFile (); - self::setReporting (); + $this->data = self::parseFile($this->config_filename); } catch (Minz_FileNotExistException $e) { - throw $e; - } catch (Minz_BadConfigurationException $e) { - throw $e; + if (is_null($default_filename)) { + throw $e; + } } - } - public static function writeFile() { - $ini_array = array( - 'general' => array( - 'environment' => self::environment(true), - 'salt' => self::$salt, - 'base_url' => self::$base_url, - 'title' => self::$title, - 'default_user' => self::$default_user, - 'allow_anonymous' => self::$allow_anonymous, - 'allow_anonymous_refresh' => self::$allow_anonymous_refresh, - 'auth_type' => self::$auth_type, - 'api_enabled' => self::$api_enabled, - 'unsafe_autologin_enabled' => self::$unsafe_autologin_enabled, - ), - 'limits' => self::$limits, - 'db' => self::$db, - ); - @rename(DATA_PATH . self::CONF_PATH_NAME, DATA_PATH . self::CONF_PATH_NAME . '.bak.php'); - $result = file_put_contents(DATA_PATH . self::CONF_PATH_NAME, "default_filename = $default_filename; + if (!is_null($this->default_filename)) { + $this->data_default = self::parseFile($this->default_filename); } - return (bool)$result; } /** - * Parse un fichier de configuration - * @exception Minz_PermissionDeniedException si le CONF_PATH_NAME n'est pas accessible - * @exception Minz_BadConfigurationException si CONF_PATH_NAME mal formaté + * Return the value of the given param. + * + * @param $key the name of the param. + * @param $default default value to return if key does not exist. + * @return the value corresponding to the key. + * @throws Minz_ConfigurationParamException if the param does not exist */ - private static function parseFile () { - $ini_array = include(DATA_PATH . self::CONF_PATH_NAME); - - if (!is_array($ini_array)) { - throw new Minz_PermissionDeniedException ( - DATA_PATH . self::CONF_PATH_NAME, - Minz_Exception::ERROR + public function param($key, $default = null) { + if (isset($this->data[$key])) { + return $this->data[$key]; + } elseif (!is_null($default)) { + return $default; + } elseif (isset($this->data_default[$key])) { + return $this->data_default[$key]; + } else { + throw new Minz_ConfigurationParamException( + $key . ' param does not exist' ); } + } - // [general] est obligatoire - if (!isset ($ini_array['general'])) { - throw new Minz_BadConfigurationException ( - '[general]', - Minz_Exception::ERROR - ); - } - $general = $ini_array['general']; + /** + * A wrapper for param(). + */ + public function __get($key) { + return $this->param($key); + } - // salt est obligatoire - if (!isset ($general['salt'])) { - if (isset($general['sel_application'])) { //v0.6 - $general['salt'] = $general['sel_application']; - } else { - throw new Minz_BadConfigurationException ( - 'salt', - Minz_Exception::ERROR - ); - } + /** + * Set or remove a param. + * + * @param $key the param name to set. + * @param $value the value to set. If null, the key is removed from the configuration. + */ + public function _param($key, $value = null) { + if (isset($this->data[$key]) && is_null($value)) { + unset($this->data[$key]); + } else { + $this->data[$key] = $value; } - self::$salt = $general['salt']; - - if (isset ($general['environment'])) { - switch ($general['environment']) { - case 'silent': - self::$environment = Minz_Configuration::SILENT; - break; - case 'development': - self::$environment = Minz_Configuration::DEVELOPMENT; - break; - case 'production': - self::$environment = Minz_Configuration::PRODUCTION; - break; - default: - if ($general['environment'] >= 0 && - $general['environment'] <= 2) { - // fallback 0.7-beta - self::$environment = $general['environment']; - } else { - throw new Minz_BadConfigurationException ( - 'environment', - Minz_Exception::ERROR - ); - } - } + } - } - if (isset ($general['base_url'])) { - self::$base_url = $general['base_url']; - } + /** + * A wrapper for _param(). + */ + public function __set($key, $value) { + $this->_param($key, $value); + } - if (isset ($general['title'])) { - self::$title = $general['title']; - } - if (isset ($general['language'])) { - self::$language = $general['language']; - } - if (isset ($general['default_user'])) { - self::$default_user = $general['default_user']; - } - if (isset ($general['auth_type'])) { - self::_authType($general['auth_type']); - } - if (isset ($general['allow_anonymous'])) { - self::$allow_anonymous = ( - ((bool)($general['allow_anonymous'])) && - ($general['allow_anonymous'] !== 'no') - ); - } - if (isset ($general['allow_anonymous_refresh'])) { - self::$allow_anonymous_refresh = ( - ((bool)($general['allow_anonymous_refresh'])) && - ($general['allow_anonymous_refresh'] !== 'no') - ); - } - if (isset ($general['api_enabled'])) { - self::$api_enabled = ( - ((bool)($general['api_enabled'])) && - ($general['api_enabled'] !== 'no') - ); - } - if (isset ($general['unsafe_autologin_enabled'])) { - self::$unsafe_autologin_enabled = ( - ((bool)($general['unsafe_autologin_enabled'])) && - ($general['unsafe_autologin_enabled'] !== 'no') - ); - } + /** + * Save the current configuration in the configuration file. + */ + public function save() { + $back_filename = $this->config_filename . '.bak.php'; + @rename($this->config_filename, $back_filename); - if (isset($ini_array['limits'])) { - $limits = $ini_array['limits']; - if (isset($limits['cache_duration'])) { - $v = intval($limits['cache_duration']); - if ($v > 0) { - self::$limits['cache_duration'] = $v; - } - } - if (isset($limits['timeout'])) { - $v = intval($limits['timeout']); - if ($v > 0) { - self::$limits['timeout'] = $v; - } - } - if (isset($limits['max_inactivity'])) { - $v = intval($limits['max_inactivity']); - if ($v > 0) { - self::$limits['max_inactivity'] = $v; - } - } - if (isset($limits['max_feeds'])) { - $v = intval($limits['max_feeds']); - if ($v > 0 && $v < Minz_Configuration::MAX_SMALL_INT) { - self::$limits['max_feeds'] = $v; - } - } - if (isset($limits['max_categories'])) { - $v = intval($limits['max_categories']); - if ($v > 0 && $v < Minz_Configuration::MAX_SMALL_INT) { - self::$limits['max_categories'] = $v; - } - } + if (file_put_contents($this->config_filename, + "data, true) . ';', + LOCK_EX) === false) { + return false; } - // Base de données - if (isset ($ini_array['db'])) { - $db = $ini_array['db']; - if (empty($db['type'])) { - throw new Minz_BadConfigurationException ( - 'type', - Minz_Exception::ERROR - ); - } - switch ($db['type']) { - case 'mysql': - if (empty($db['host'])) { - throw new Minz_BadConfigurationException ( - 'host', - Minz_Exception::ERROR - ); - } - if (empty($db['user'])) { - throw new Minz_BadConfigurationException ( - 'user', - Minz_Exception::ERROR - ); - } - if (!isset($db['password'])) { - throw new Minz_BadConfigurationException ( - 'password', - Minz_Exception::ERROR - ); - } - if (empty($db['base'])) { - throw new Minz_BadConfigurationException ( - 'base', - Minz_Exception::ERROR - ); - } - self::$db['host'] = $db['host']; - self::$db['user'] = $db['user']; - self::$db['password'] = $db['password']; - self::$db['base'] = $db['base']; - if (isset($db['prefix'])) { - self::$db['prefix'] = $db['prefix']; - } - break; - case 'sqlite': - self::$db['host'] = ''; - self::$db['user'] = ''; - self::$db['password'] = ''; - self::$db['base'] = ''; - self::$db['prefix'] = ''; - break; - default: - throw new Minz_BadConfigurationException ( - 'type', - Minz_Exception::ERROR - ); - break; - } - self::$db['type'] = $db['type']; + // Clear PHP 5.5+ cache for include + if (function_exists('opcache_invalidate')) { + opcache_invalidate($this->config_filename); } - } - private static function setReporting() { - switch (self::$environment) { - case self::PRODUCTION: - error_reporting(E_ALL); - ini_set('display_errors','Off'); - ini_set('log_errors', 'On'); - break; - case self::DEVELOPMENT: - error_reporting(E_ALL); - ini_set('display_errors','On'); - ini_set('log_errors', 'On'); - break; - case self::SILENT: - error_reporting(0); - break; - } + return true; } } diff --git a/lib/Minz/ConfigurationException.php b/lib/Minz/ConfigurationException.php new file mode 100644 index 000000000..f294c3341 --- /dev/null +++ b/lib/Minz/ConfigurationException.php @@ -0,0 +1,8 @@ + en fonction de l'environment */ private static function processLogs ($logs) { - $env = Minz_Configuration::environment (); + $conf = Minz_Configuration::get('system'); + $env = $conf->general['environment']; $logs_ok = array (); $error = array (); $warning = array (); @@ -98,10 +99,10 @@ class Minz_Error { $notice = $logs['notice']; } - if ($env == Minz_Configuration::PRODUCTION) { + if ($env == 'production') { $logs_ok = $error; } - if ($env == Minz_Configuration::DEVELOPMENT) { + if ($env == 'development') { $logs_ok = array_merge ($error, $warning, $notice); } diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php index 3dac1e438..974cf4260 100644 --- a/lib/Minz/FrontController.php +++ b/lib/Minz/FrontController.php @@ -31,9 +31,12 @@ class Minz_FrontController { */ public function __construct () { try { - Minz_Configuration::init (); + Minz_Configuration::register('system', + DATA_PATH . '/config.php', + DATA_PATH . '/config.default.php'); + $this->setReporting(); - Minz_Request::init (); + Minz_Request::init(); $url = $this->buildUrl(); $url['params'] = array_merge ( @@ -110,4 +113,23 @@ class Minz_FrontController { } exit ('### Application problem ###
      '."\n".$txt); } + + private function setReporting() { + $conf = Minz_Configuration::get('system'); + switch($conf->general['environment']) { + case 'production': + error_reporting(E_ALL); + ini_set('display_errors','Off'); + ini_set('log_errors', 'On'); + break; + case 'development': + error_reporting(E_ALL); + ini_set('display_errors','On'); + ini_set('log_errors', 'On'); + break; + case 'silent': + error_reporting(0); + break; + } + } } diff --git a/lib/Minz/Log.php b/lib/Minz/Log.php index d19edc1dc..2063efe7e 100644 --- a/lib/Minz/Log.php +++ b/lib/Minz/Log.php @@ -31,10 +31,15 @@ class Minz_Log { * @param $file_name fichier de log */ public static function record ($information, $level, $file_name = null) { - $env = Minz_Configuration::environment (); + try { + $conf = Minz_Configuration::get('system'); + $env = $conf->general['environment']; + } catch (Minz_ConfigurationException $e) { + $env = 'production'; + } - if (! ($env === Minz_Configuration::SILENT - || ($env === Minz_Configuration::PRODUCTION + if (! ($env === 'silent' + || ($env === 'production' && ($level >= Minz_Log::NOTICE)))) { if ($file_name === null) { $file_name = join_path(USERS_PATH, Minz_Session::param('currentUser', '_'), 'log.txt'); diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index 118d89ad2..ac7a1bed7 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -44,7 +44,8 @@ class Minz_ModelPdo { return; } - $db = Minz_Configuration::dataBase(); + $conf = Minz_Configuration::get('system'); + $db = $conf->db; if ($currentUser === null) { $currentUser = Minz_Session::param('currentUser', '_'); diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index 4b97a3caf..5f2f6a858 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -96,7 +96,8 @@ class Minz_Request { * @return la base de l'url */ public static function getBaseUrl() { - $defaultBaseUrl = Minz_Configuration::baseUrl(); + $conf = Minz_Configuration::get('system'); + $defaultBaseUrl = $conf->general['base_url']; if (!empty($defaultBaseUrl)) { return $defaultBaseUrl; } elseif (isset($_SERVER['REQUEST_URI'])) { diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index e7efb8665..7525e95cc 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -28,7 +28,8 @@ class Minz_Translate { * Load $lang_name and $lang_path based on configuration and selected language. */ public static function init() { - $l = Minz_Configuration::language(); + $conf = Minz_Configuration::get('system'); + $l = $conf->general['language']; self::$lang_name = Minz_Session::param('language', $l); self::$lang_path = APP_PATH . '/i18n/' . self::$lang_name . '/'; } diff --git a/lib/Minz/View.php b/lib/Minz/View.php index b40448491..24ad630d0 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -28,7 +28,9 @@ class Minz_View { public function __construct () { $this->change_view(Minz_Request::controllerName(), Minz_Request::actionName()); - self::$title = Minz_Configuration::title (); + + $conf = Minz_Configuration::get('system'); + self::$title = $conf->general['title']; } /** diff --git a/p/i/index.php b/p/i/index.php index 009d56bc3..d3fc0b37c 100755 --- a/p/i/index.php +++ b/p/i/index.php @@ -33,7 +33,7 @@ if (file_exists(DATA_PATH . '/do-install.txt')) { $currentUser = Minz_Session::param('currentUser', ''); $dateLastModification = $currentUser === '' ? time() : max( @filemtime(join_path(USERS_PATH, $currentUser, 'log.txt')), - @filemtime(join_path(DATA_PATH . 'config.php')) + @filemtime(join_path(DATA_PATH, 'config.php')) ); if (httpConditional($dateLastModification, 0, 0, false, PHP_COMPRESSION, true)) { exit(); //No need to send anything -- cgit v1.2.3 From 7cca47d1ab5838f5440b1a1e08fa4c0d43989664 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 5 Jan 2015 22:43:15 +0100 Subject: Change name of user configuration var in Context - FreshRSS_Context::$conf is replaced by FreshRSS_Context::$user_conf - Introduce FreshRSS_Context::$system_conf - Remove FreshRSS_Configuration object See https://github.com/FreshRSS/FreshRSS/issues/730 --- app/Controllers/authController.php | 24 +- app/Controllers/categoryController.php | 10 +- app/Controllers/configureController.php | 94 ++++---- app/Controllers/entryController.php | 4 +- app/Controllers/feedController.php | 18 +- app/Controllers/importExportController.php | 4 +- app/Controllers/indexController.php | 12 +- app/Controllers/javascriptController.php | 2 +- app/Controllers/userController.php | 18 +- app/FreshRSS.php | 7 +- app/Models/Configuration.php | 345 ----------------------------- app/Models/Context.php | 20 +- app/layout/aside_feed.phtml | 6 +- app/layout/layout.phtml | 2 +- app/layout/nav_menu.phtml | 10 +- app/views/auth/index.phtml | 4 +- app/views/configure/archiving.phtml | 10 +- app/views/configure/display.phtml | 30 +-- app/views/configure/queries.phtml | 4 +- app/views/configure/reading.phtml | 44 ++-- app/views/configure/sharing.phtml | 6 +- app/views/configure/shortcut.phtml | 2 +- app/views/helpers/javascript_vars.phtml | 14 +- app/views/helpers/pagination.phtml | 2 +- app/views/index/global.phtml | 4 +- app/views/index/normal.phtml | 32 +-- app/views/index/reader.phtml | 4 +- app/views/user/manage.phtml | 6 +- app/views/user/profile.phtml | 2 +- 29 files changed, 196 insertions(+), 544 deletions(-) delete mode 100644 app/Models/Configuration.php diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index f68ea8da5..3a1ad4605 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -27,12 +27,11 @@ class FreshRSS_auth_Controller extends Minz_ActionController { if (Minz_Request::isPost()) { $ok = true; - $system_conf = Minz_Configuration::get('system'); - $general = $system_conf->general; - $current_token = FreshRSS_Context::$conf->token; + $general = FreshRSS_Context::$system_conf->general; + $current_token = FreshRSS_Context::$user_conf->token; $token = Minz_Request::param('token', $current_token); - FreshRSS_Context::$conf->_token($token); - $ok &= FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->_token($token); + $ok &= FreshRSS_Context::$user_conf->save(); $anon = Minz_Request::param('anon_access', false); $anon = ((bool)$anon) && ($anon !== 'no'); @@ -81,8 +80,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); } - $conf = Minz_Configuration::get('system'); - $auth_type = $conf->general['auth_type']; + $auth_type = FreshRSS_Context::$system_conf->general['auth_type']; switch ($auth_type) { case 'form': Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin')); @@ -120,12 +118,12 @@ class FreshRSS_auth_Controller extends Minz_ActionController { $file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'); Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime)); - $conf = Minz_Configuration::get('system'); - if (Minz_Request::isPost()) { $nonce = Minz_Session::param('nonce'); $username = Minz_Request::param('username', ''); $challenge = Minz_Request::param('challenge', ''); + + // TODO #730: change the way to get the configuration try { $conf = new FreshRSS_Configuration($username); } catch(Minz_Exception $e) { @@ -162,7 +160,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { Minz_Request::bad(_t('feedback.auth.login.invalid'), array('c' => 'auth', 'a' => 'login')); } - } elseif ($conf->general['unsafe_autologin_enabled']) { + } elseif (FreshRSS_Context::$system_conf->general['unsafe_autologin_enabled']) { $username = Minz_Request::param('u', ''); $password = Minz_Request::param('p', ''); Minz_Request::_param('p'); @@ -171,6 +169,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { return; } + // TODO #730: change the way to get the configuration try { $conf = new FreshRSS_Configuration($username); } catch(Minz_Exception $e) { @@ -243,6 +242,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { $persona_file = DATA_PATH . '/persona/' . $email . '.txt'; if (($current_user = @file_get_contents($persona_file)) !== false) { $current_user = trim($current_user); + // TODO #730: change the way to get the configuration try { $conf = new FreshRSS_Configuration($current_user); $login_ok = strcasecmp($email, $conf->mail_login) === 0; @@ -301,7 +301,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { $this->view->no_form = false; // Enable changement of auth only if Persona! - if (Minz_Configuration::authType() != 'persona') { + if (FreshRSS_Context::$system_conf->general['auth_type'] != 'persona') { $this->view->message = array( 'status' => 'bad', 'title' => _t('gen.short.damn'), @@ -311,6 +311,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { return; } + // TODO #730 $conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser()); // Admin user must have set its master password. if (!$conf->passwordHash) { @@ -335,6 +336,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { ); if ($ok) { + // TODO #730 Minz_Configuration::_authType('form'); $ok = Minz_Configuration::writeFile(); diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php index 5f1beae90..c90e55ea7 100644 --- a/app/Controllers/categoryController.php +++ b/app/Controllers/categoryController.php @@ -30,7 +30,7 @@ class FreshRSS_category_Controller extends Minz_ActionController { $catDAO = new FreshRSS_CategoryDAO(); $url_redirect = array('c' => 'subscription', 'a' => 'index'); - $limits = Minz_Configuration::limits(); + $limits = FreshRSS_Context::$system_conf->limits; $this->view->categories = $catDAO->listCategories(false); if (count($this->view->categories) >= $limits['max_categories']) { @@ -141,8 +141,8 @@ class FreshRSS_category_Controller extends Minz_ActionController { } // Remove related queries. - FreshRSS_Context::$conf->remove_query_by_get('c_' . $id); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->remove_query_by_get('c_' . $id); + FreshRSS_Context::$user_conf->save(); Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect); } @@ -177,9 +177,9 @@ class FreshRSS_category_Controller extends Minz_ActionController { // Remove related queries foreach ($feeds as $feed) { - FreshRSS_Context::$conf->remove_query_by_get('f_' . $feed->id()); + FreshRSS_Context::$user_conf->remove_query_by_get('f_' . $feed->id()); } - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->save(); Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect); } else { diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index feb5483fb..6e7a40ea6 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -41,23 +41,23 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function displayAction() { if (Minz_Request::isPost()) { - FreshRSS_Context::$conf->_language(Minz_Request::param('language', 'en')); - FreshRSS_Context::$conf->_theme(Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme)); - FreshRSS_Context::$conf->_content_width(Minz_Request::param('content_width', 'thin')); - FreshRSS_Context::$conf->_topline_read(Minz_Request::param('topline_read', false)); - FreshRSS_Context::$conf->_topline_favorite(Minz_Request::param('topline_favorite', false)); - FreshRSS_Context::$conf->_topline_date(Minz_Request::param('topline_date', false)); - FreshRSS_Context::$conf->_topline_link(Minz_Request::param('topline_link', false)); - FreshRSS_Context::$conf->_bottomline_read(Minz_Request::param('bottomline_read', false)); - FreshRSS_Context::$conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false)); - FreshRSS_Context::$conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false)); - FreshRSS_Context::$conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false)); - FreshRSS_Context::$conf->_bottomline_date(Minz_Request::param('bottomline_date', false)); - FreshRSS_Context::$conf->_bottomline_link(Minz_Request::param('bottomline_link', false)); - FreshRSS_Context::$conf->_html5_notif_timeout(Minz_Request::param('html5_notif_timeout', 0)); - FreshRSS_Context::$conf->save(); - - Minz_Session::_param('language', FreshRSS_Context::$conf->language); + FreshRSS_Context::$user_conf->_language(Minz_Request::param('language', 'en')); + FreshRSS_Context::$user_conf->_theme(Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme)); + FreshRSS_Context::$user_conf->_content_width(Minz_Request::param('content_width', 'thin')); + FreshRSS_Context::$user_conf->_topline_read(Minz_Request::param('topline_read', false)); + FreshRSS_Context::$user_conf->_topline_favorite(Minz_Request::param('topline_favorite', false)); + FreshRSS_Context::$user_conf->_topline_date(Minz_Request::param('topline_date', false)); + FreshRSS_Context::$user_conf->_topline_link(Minz_Request::param('topline_link', false)); + FreshRSS_Context::$user_conf->_bottomline_read(Minz_Request::param('bottomline_read', false)); + FreshRSS_Context::$user_conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false)); + FreshRSS_Context::$user_conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false)); + FreshRSS_Context::$user_conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false)); + FreshRSS_Context::$user_conf->_bottomline_date(Minz_Request::param('bottomline_date', false)); + FreshRSS_Context::$user_conf->_bottomline_link(Minz_Request::param('bottomline_link', false)); + FreshRSS_Context::$user_conf->_html5_notif_timeout(Minz_Request::param('html5_notif_timeout', 0)); + FreshRSS_Context::$user_conf->save(); + + Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); Minz_Translate::reset(); invalidateHttpCache(); @@ -100,28 +100,28 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function readingAction() { if (Minz_Request::isPost()) { - FreshRSS_Context::$conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); - FreshRSS_Context::$conf->_view_mode(Minz_Request::param('view_mode', 'normal')); - FreshRSS_Context::$conf->_default_view(Minz_Request::param('default_view', 'adaptive')); - FreshRSS_Context::$conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); - FreshRSS_Context::$conf->_display_posts(Minz_Request::param('display_posts', false)); - FreshRSS_Context::$conf->_display_categories(Minz_Request::param('display_categories', false)); - FreshRSS_Context::$conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false)); - FreshRSS_Context::$conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); - FreshRSS_Context::$conf->_lazyload(Minz_Request::param('lazyload', false)); - FreshRSS_Context::$conf->_sticky_post(Minz_Request::param('sticky_post', false)); - FreshRSS_Context::$conf->_reading_confirm(Minz_Request::param('reading_confirm', false)); - FreshRSS_Context::$conf->_auto_remove_article(Minz_Request::param('auto_remove_article', false)); - FreshRSS_Context::$conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); - FreshRSS_Context::$conf->_mark_when(array( + FreshRSS_Context::$user_conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); + FreshRSS_Context::$user_conf->_view_mode(Minz_Request::param('view_mode', 'normal')); + FreshRSS_Context::$user_conf->_default_view(Minz_Request::param('default_view', 'adaptive')); + FreshRSS_Context::$user_conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); + FreshRSS_Context::$user_conf->_display_posts(Minz_Request::param('display_posts', false)); + FreshRSS_Context::$user_conf->_display_categories(Minz_Request::param('display_categories', false)); + FreshRSS_Context::$user_conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false)); + FreshRSS_Context::$user_conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); + FreshRSS_Context::$user_conf->_lazyload(Minz_Request::param('lazyload', false)); + 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->_sort_order(Minz_Request::param('sort_order', 'DESC')); + FreshRSS_Context::$user_conf->_mark_when(array( 'article' => Minz_Request::param('mark_open_article', false), 'site' => Minz_Request::param('mark_open_site', false), 'scroll' => Minz_Request::param('mark_scroll', false), 'reception' => Minz_Request::param('mark_upon_reception', false), )); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->save(); - Minz_Session::_param('language', FreshRSS_Context::$conf->language); + Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); Minz_Translate::reset(); invalidateHttpCache(); @@ -142,8 +142,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController { public function sharingAction() { if (Minz_Request::isPost()) { $params = Minz_Request::params(); - FreshRSS_Context::$conf->_sharing($params['share']); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->_sharing($params['share']); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); Minz_Request::good(_t('feedback.conf.updated'), @@ -184,8 +184,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController { } } - FreshRSS_Context::$conf->_shortcuts($shortcuts_ok); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->_shortcuts($shortcuts_ok); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); Minz_Request::good(_t('feedback.conf.shortcuts_updated'), @@ -212,10 +212,10 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function archivingAction() { if (Minz_Request::isPost()) { - FreshRSS_Context::$conf->_old_entries(Minz_Request::param('old_entries', 3)); - FreshRSS_Context::$conf->_keep_history_default(Minz_Request::param('keep_history_default', 0)); - FreshRSS_Context::$conf->_ttl_default(Minz_Request::param('ttl_default', -2)); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->_old_entries(Minz_Request::param('old_entries', 3)); + FreshRSS_Context::$user_conf->_keep_history_default(Minz_Request::param('keep_history_default', 0)); + FreshRSS_Context::$user_conf->_ttl_default(Minz_Request::param('ttl_default', -2)); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); Minz_Request::good(_t('feedback.conf.updated'), @@ -252,8 +252,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $query['name'] = _t('conf.query.number', $key + 1); } } - FreshRSS_Context::$conf->_queries($queries); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->_queries($queries); + FreshRSS_Context::$user_conf->save(); Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'queries')); @@ -261,7 +261,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $this->view->query_get = array(); $cat_dao = new FreshRSS_CategoryDAO(); $feed_dao = FreshRSS_Factory::createFeedDao(); - foreach (FreshRSS_Context::$conf->queries as $key => $query) { + foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { if (!isset($query['get'])) { continue; } @@ -329,7 +329,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function addQueryAction() { $whitelist = array('get', 'order', 'name', 'search', 'state'); - $queries = FreshRSS_Context::$conf->queries; + $queries = FreshRSS_Context::$user_conf->queries; $query = Minz_Request::params(); $query['name'] = _t('conf.query.number', count($queries) + 1); foreach ($query as $key => $value) { @@ -338,8 +338,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController { } } $queries[] = $query; - FreshRSS_Context::$conf->_queries($queries); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->_queries($queries); + FreshRSS_Context::$user_conf->save(); Minz_Request::good(_t('feedback.conf.query_created', $query['name']), array('c' => 'configure', 'a' => 'queries')); diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index aae08c413..1d9989f40 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -154,7 +154,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { public function purgeAction() { @set_time_limit(300); - $nb_month_old = max(FreshRSS_Context::$conf->old_entries, 1); + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -168,7 +168,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { if ($feed_history == -2) { // TODO: -2 must be a constant! // -2 means we take the default value from configuration - $feed_history = FreshRSS_Context::$conf->keep_history_default; + $feed_history = FreshRSS_Context::$user_conf->keep_history_default; } if ($feed_history >= 0) { diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 121cb8921..92a1e3bf8 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -14,7 +14,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Token is useful in the case that anonymous refresh is forbidden // and CRON task cannot be used with php command so the user can // set a CRON task to refresh his feeds by using token inside url - $token = FreshRSS_Context::$conf->token; + $token = FreshRSS_Context::$user_conf->token; $token_param = Minz_Request::param('token', ''); $token_is_ok = ($token != '' && $token == $token_param); $action = Minz_Request::actionName(); @@ -161,14 +161,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->_id($id); $feed->faviconPrepare(); - $is_read = FreshRSS_Context::$conf->mark_when['reception'] ? 1 : 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; $entryDAO = FreshRSS_Factory::createEntryDao(); // We want chronological order and SimplePie uses reverse order. $entries = array_reverse($feed->entries()); // Calculate date of oldest entries we accept in DB. - $nb_month_old = FreshRSS_Context::$conf->old_entries; + $nb_month_old = FreshRSS_Context::$user_conf->old_entries; $date_min = time() - (3600 * 24 * 30 * $nb_month_old); // Use a shared statement and a transaction to improve a LOT the @@ -272,15 +272,15 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feeds[] = $feed; } } else { - $feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$conf->ttl_default); + $feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } // Calculate date of oldest entries we accept in DB. - $nb_month_old = max(FreshRSS_Context::$conf->old_entries, 1); + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); $updated_feeds = 0; - $is_read = FreshRSS_Context::$conf->mark_when['reception'] ? 1 : 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { if (!$feed->lock()) { Minz_Log::notice('Feed already being actualized: ' . $feed->url()); @@ -302,7 +302,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if ($feed_history == -2) { // TODO: -2 must be a constant! // -2 means we take the default value from configuration - $feed_history = FreshRSS_Context::$conf->keep_history_default; + $feed_history = FreshRSS_Context::$user_conf->keep_history_default; } // We want chronological order and SimplePie uses reverse order. @@ -476,8 +476,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // TODO: Delete old favicon // Remove related queries - FreshRSS_Context::$conf->remove_query_by_get('f_' . $id); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->remove_query_by_get('f_' . $id); + FreshRSS_Context::$user_conf->save(); Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url); } else { diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 6eefa0f6f..334f33d6a 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -315,7 +315,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return true; } - $is_read = FreshRSS_Context::$conf->mark_when['reception'] ? 1 : 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; $google_compliant = strpos($article_object['id'], 'com.google') !== false; @@ -532,7 +532,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->view->type = 'feed/' . $feed->id(); $this->view->entries = $this->entryDAO->listWhere( 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', - FreshRSS_Context::$conf->posts_per_page + FreshRSS_Context::$user_conf->posts_per_page ); $this->view->feed = $feed; } diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 33cd2843c..14f3f4f4b 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -9,7 +9,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { * This action only redirect on the default view mode (normal or global) */ public function indexAction() { - $prefered_output = FreshRSS_Context::$conf->view_mode; + $prefered_output = FreshRSS_Context::$user_conf->view_mode; Minz_Request::forward(array( 'c' => 'index', 'a' => $prefered_output @@ -109,7 +109,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { * This action displays the RSS feed of FreshRSS. */ public function rssAction() { - $token = FreshRSS_Context::$conf->token; + $token = FreshRSS_Context::$user_conf->token; $token_param = Minz_Request::param('token', ''); $token_is_ok = ($token != '' && $token === $token_param); @@ -160,10 +160,10 @@ class FreshRSS_index_Controller extends Minz_ActionController { FreshRSS_Context::_get(Minz_Request::param('get', 'a')); FreshRSS_Context::$state = Minz_Request::param( - 'state', FreshRSS_Context::$conf->default_state + 'state', FreshRSS_Context::$user_conf->default_state ); $state_forced_by_user = Minz_Request::param('state', false) !== false; - if (FreshRSS_Context::$conf->default_view === 'adaptive' && + if (FreshRSS_Context::$user_conf->default_view === 'adaptive' && FreshRSS_Context::$get_unread <= 0 && !FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ) && !$state_forced_by_user) { @@ -172,10 +172,10 @@ class FreshRSS_index_Controller extends Minz_ActionController { FreshRSS_Context::$search = Minz_Request::param('search', ''); FreshRSS_Context::$order = Minz_Request::param( - 'order', FreshRSS_Context::$conf->sort_order + 'order', FreshRSS_Context::$user_conf->sort_order ); FreshRSS_Context::$number = Minz_Request::param( - 'nb', FreshRSS_Context::$conf->posts_per_page + 'nb', FreshRSS_Context::$user_conf->posts_per_page ); FreshRSS_Context::$first_id = Minz_Request::param('next', ''); } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index 113f58ea9..b178801d4 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -8,7 +8,7 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { public function actualizeAction() { header('Content-Type: text/javascript; charset=UTF-8'); $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$conf->ttl_default); + $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } public function nbUnreadsPerFeedAction() { diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 1b1ccaac9..58181bfb0 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -39,9 +39,9 @@ class FreshRSS_user_Controller extends Minz_ActionController { $passwordPlain = ''; $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js $ok &= ($passwordHash != ''); - FreshRSS_Context::$conf->_passwordHash($passwordHash); + FreshRSS_Context::$user_conf->_passwordHash($passwordHash); } - Minz_Session::_param('passwordHash', FreshRSS_Context::$conf->passwordHash); + Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash); $passwordPlain = Minz_Request::param('apiPasswordPlain', '', true); if ($passwordPlain != '') { @@ -52,17 +52,17 @@ class FreshRSS_user_Controller extends Minz_ActionController { $passwordPlain = ''; $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js $ok &= ($passwordHash != ''); - FreshRSS_Context::$conf->_apiPasswordHash($passwordHash); + FreshRSS_Context::$user_conf->_apiPasswordHash($passwordHash); } // TODO: why do we need of hasAccess here? if (FreshRSS_Auth::hasAccess('admin')) { - FreshRSS_Context::$conf->_mail_login(Minz_Request::param('mail_login', '', true)); + FreshRSS_Context::$user_conf->_mail_login(Minz_Request::param('mail_login', '', true)); } - $email = FreshRSS_Context::$conf->mail_login; + $email = FreshRSS_Context::$user_conf->mail_login; Minz_Session::_param('mail', $email); - $ok &= FreshRSS_Context::$conf->save(); + $ok &= FreshRSS_Context::$user_conf->save(); if ($email != '') { $personaFile = DATA_PATH . '/persona/' . $email . '.txt'; @@ -108,10 +108,10 @@ class FreshRSS_user_Controller extends Minz_ActionController { $db = Minz_Configuration::dataBase(); require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$conf->language); - $languages = FreshRSS_Context::$conf->availableLanguages(); + $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language); + $languages = FreshRSS_Context::$user_conf->availableLanguages(); if (!isset($languages[$new_user_language])) { - $new_user_language = FreshRSS_Context::$conf->language; + $new_user_language = FreshRSS_Context::$user_conf->language; } $new_user_name = Minz_Request::param('new_user_name'); diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 455f2fefd..b22bfdb4b 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -32,7 +32,7 @@ class FreshRSS extends Minz_FrontController { FreshRSS_Context::init(); // Init i18n. - Minz_Session::_param('language', FreshRSS_Context::$conf->language); + Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); Minz_Translate::init(); $this->loadStylesAndScripts(); @@ -41,7 +41,7 @@ class FreshRSS extends Minz_FrontController { } private function loadStylesAndScripts() { - $theme = FreshRSS_Themes::load(FreshRSS_Context::$conf->theme); + $theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme); if ($theme) { foreach($theme['files'] as $file) { if ($file[0] === '_') { @@ -62,8 +62,7 @@ class FreshRSS extends Minz_FrontController { Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js'))); - $conf = Minz_Configuration::get('system'); - if ($conf->general['auth_type'] === 'persona') { + if (FreshRSS_Context::$system_conf->general['auth_type'] === 'persona') { // TODO move it in a plugin // Needed for login AND logout with Persona. Minz_View::appendScript('https://login.persona.org/include.js'); diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php deleted file mode 100644 index 8bba8f777..000000000 --- a/app/Models/Configuration.php +++ /dev/null @@ -1,345 +0,0 @@ - 'en', - 'old_entries' => 3, - 'keep_history_default' => 0, - 'ttl_default' => 3600, - 'mail_login' => '', - 'token' => '', - 'passwordHash' => '', //CRYPT_BLOWFISH - 'apiPasswordHash' => '', //CRYPT_BLOWFISH - 'posts_per_page' => 20, - 'view_mode' => 'normal', - 'default_view' => 'adaptive', - 'default_state' => FreshRSS_Entry::STATE_NOT_READ, - 'auto_load_more' => true, - 'display_posts' => false, - 'display_categories' => false, - 'hide_read_feeds' => true, - 'onread_jump_next' => true, - 'lazyload' => true, - 'sticky_post' => true, - 'reading_confirm' => false, - 'auto_remove_article' => false, - 'sort_order' => 'DESC', - 'anon_access' => false, - 'mark_when' => array( - 'article' => true, - 'site' => true, - 'scroll' => false, - 'reception' => false, - ), - 'theme' => 'Origine', - 'content_width' => 'thin', - 'shortcuts' => array( - 'mark_read' => 'r', - 'mark_favorite' => 'f', - 'go_website' => 'space', - 'next_entry' => 'j', - 'prev_entry' => 'k', - 'first_entry' => 'home', - 'last_entry' => 'end', - 'collapse_entry' => 'c', - 'load_more' => 'm', - 'auto_share' => 's', - 'focus_search' => 'a', - 'user_filter' => 'u', - 'help' => 'f1', - 'close_dropdown' => 'escape', - ), - 'topline_read' => true, - 'topline_favorite' => true, - 'topline_date' => true, - 'topline_link' => true, - 'bottomline_read' => true, - 'bottomline_favorite' => true, - 'bottomline_sharing' => true, - 'bottomline_tags' => true, - 'bottomline_date' => true, - 'bottomline_link' => true, - 'sharing' => array(), - 'queries' => array(), - 'html5_notif_timeout' => 0, - ); - - private $available_languages = array( - 'en' => 'English', - 'fr' => 'Français', - ); - - private $shares; - - public function __construct($user) { - $this->filename = join_path(DATA_PATH, 'users', $user, 'config.php'); - - $data = @include($this->filename); - if (!is_array($data)) { - throw new Minz_PermissionDeniedException($this->filename); - } - - foreach ($data as $key => $value) { - if (isset($this->data[$key])) { - $function = '_' . $key; - $this->$function($value); - } - } - $this->data['user'] = $user; - - $this->shares = join_path(DATA_PATH, 'shares.php'); - - $shares = @include($this->shares); - if (!is_array($shares)) { - throw new Minz_PermissionDeniedException($this->shares); - } - - $this->data['shares'] = $shares; - } - - public function save() { - @rename($this->filename, $this->filename . '.bak.php'); - unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration - if (file_put_contents($this->filename, "data, true) . ';', LOCK_EX) === false) { - throw new Minz_PermissionDeniedException($this->filename); - } - if (function_exists('opcache_invalidate')) { - opcache_invalidate($this->filename); //Clear PHP 5.5+ cache for include - } - invalidateHttpCache(); - return true; - } - - public function __get($name) { - if (array_key_exists($name, $this->data)) { - return $this->data[$name]; - } else { - $trace = debug_backtrace(); - trigger_error('Undefined FreshRSS_Configuration->' . $name . 'in ' . $trace[0]['file'] . ' line ' . $trace[0]['line'], E_USER_NOTICE); //TODO: Use Minz exceptions - return null; - } - } - - public function availableLanguages() { - return $this->available_languages; - } - - public function remove_query_by_get($get) { - $final_queries = array(); - foreach ($this->queries as $key => $query) { - if (empty($query['get']) || $query['get'] !== $get) { - $final_queries[$key] = $query; - } - } - $this->_queries($final_queries); - } - - public function _language($value) { - if (!isset($this->available_languages[$value])) { - $value = 'en'; - } - $this->data['language'] = $value; - } - public function _posts_per_page($value) { - $value = intval($value); - $this->data['posts_per_page'] = $value > 0 ? $value : 10; - } - public function _view_mode($value) { - if ($value === 'global' || $value === 'reader') { - $this->data['view_mode'] = $value; - } else { - $this->data['view_mode'] = 'normal'; - } - } - public function _default_view($value) { - switch ($value) { - case 'all': - $this->data['default_view'] = $value; - $this->data['default_state'] = (FreshRSS_Entry::STATE_READ + - FreshRSS_Entry::STATE_NOT_READ); - break; - case 'adaptive': - case 'unread': - default: - $this->data['default_view'] = $value; - $this->data['default_state'] = FreshRSS_Entry::STATE_NOT_READ; - } - } - public function _default_state($value) { - $this->data['default_state'] = (int)$value; - } - - public function _display_posts($value) { - $this->data['display_posts'] = ((bool)$value) && $value !== 'no'; - } - public function _display_categories($value) { - $this->data['display_categories'] = ((bool)$value) && $value !== 'no'; - } - public function _hide_read_feeds($value) { - $this->data['hide_read_feeds'] = (bool)$value; - } - public function _onread_jump_next($value) { - $this->data['onread_jump_next'] = ((bool)$value) && $value !== 'no'; - } - public function _lazyload($value) { - $this->data['lazyload'] = ((bool)$value) && $value !== 'no'; - } - public function _sticky_post($value) { - $this->data['sticky_post'] = ((bool)$value) && $value !== 'no'; - } - public function _reading_confirm($value) { - $this->data['reading_confirm'] = ((bool)$value) && $value !== 'no'; - } - public function _auto_remove_article($value) { - $this->data['auto_remove_article'] = ((bool)$value) && $value !== 'no'; - } - public function _sort_order($value) { - $this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC'; - } - public function _old_entries($value) { - $value = intval($value); - $this->data['old_entries'] = $value > 0 ? $value : 3; - } - public function _keep_history_default($value) { - $value = intval($value); - $this->data['keep_history_default'] = $value >= -1 ? $value : 0; - } - public function _ttl_default($value) { - $value = intval($value); - $this->data['ttl_default'] = $value >= -1 ? $value : 3600; - } - public function _shortcuts($values) { - foreach ($values as $key => $value) { - if (isset($this->data['shortcuts'][$key])) { - $this->data['shortcuts'][$key] = $value; - } - } - } - public function _passwordHash($value) { - $this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; - } - public function _apiPasswordHash($value) { - $this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; - } - public function _mail_login($value) { - $value = filter_var($value, FILTER_VALIDATE_EMAIL); - if ($value) { - $this->data['mail_login'] = $value; - } else { - $this->data['mail_login'] = ''; - } - } - public function _anon_access($value) { - $this->data['anon_access'] = ((bool)$value) && $value !== 'no'; - } - public function _mark_when($values) { - foreach ($values as $key => $value) { - if (isset($this->data['mark_when'][$key])) { - $this->data['mark_when'][$key] = ((bool)$value) && $value !== 'no'; - } - } - } - public function _sharing($values) { - $this->data['sharing'] = array(); - $unique = array(); - foreach ($values as $value) { - if (!is_array($value)) { - continue; - } - - // Verify URL and add default value when needed - if (isset($value['url'])) { - $is_url = ( - filter_var($value['url'], FILTER_VALIDATE_URL) || - (version_compare(PHP_VERSION, '5.3.3', '<') && - (strpos($value, '-') > 0) && - ($value === filter_var($value, FILTER_SANITIZE_URL))) - ); //PHP bug #51192 - if (!$is_url) { - continue; - } - } else { - $value['url'] = null; - } - - // Add a default name - if (empty($value['name'])) { - $value['name'] = $value['type']; - } - - $json_value = json_encode($value); - if (!in_array($json_value, $unique)) { - $unique[] = $json_value; - $this->data['sharing'][] = $value; - } - } - } - public function _queries($values) { - $this->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)); - - $this->data['queries'][] = $value; - } - } - public function _theme($value) { - $this->data['theme'] = $value; - } - public function _content_width($value) { - if ($value === 'medium' || - $value === 'large' || - $value === 'no_limit') { - $this->data['content_width'] = $value; - } else { - $this->data['content_width'] = 'thin'; - } - } - - public function _html5_notif_timeout($value) { - $value = intval($value); - $this->data['html5_notif_timeout'] = $value >= 0 ? $value : 0; - } - - public function _token($value) { - $this->data['token'] = $value; - } - public function _auto_load_more($value) { - $this->data['auto_load_more'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_read($value) { - $this->data['topline_read'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_favorite($value) { - $this->data['topline_favorite'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_date($value) { - $this->data['topline_date'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_link($value) { - $this->data['topline_link'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_read($value) { - $this->data['bottomline_read'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_favorite($value) { - $this->data['bottomline_favorite'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_sharing($value) { - $this->data['bottomline_sharing'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_tags($value) { - $this->data['bottomline_tags'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_date($value) { - $this->data['bottomline_date'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_link($value) { - $this->data['bottomline_link'] = ((bool)$value) && $value !== 'no'; - } -} diff --git a/app/Models/Context.php b/app/Models/Context.php index 9bbad9857..1c770c756 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -5,7 +5,8 @@ * useful functions associated to the current view state. */ class FreshRSS_Context { - public static $conf = null; + public static $user_conf = null; + public static $system_conf = null; public static $categories = array(); public static $name = ''; @@ -37,17 +38,12 @@ class FreshRSS_Context { /** * Initialize the context. * - * Set the correct $conf and $categories variables. + * Set the correct configurations and $categories variables. */ public static function init() { // Init configuration. - try { - self::$conf = Minz_Configuration::get('user'); - } catch(Minz_Exception $e) { - $current_user = Minz_Session::param('currentUser', '_'); - Minz_Log::error('Cannot load configuration file of user `' . $current_user . '`'); - die($e->getMessage()); - } + self::$system_conf = Minz_Configuration::get('system'); + self::$user_conf = Minz_Configuration::get('user'); $catDAO = new FreshRSS_CategoryDAO(); self::$categories = $catDAO->listCategories(); @@ -198,7 +194,7 @@ class FreshRSS_Context { // By default, $next_get == $get self::$next_get = $get; - if (self::$conf->onread_jump_next && strlen($get) > 2) { + if (self::$user_conf->onread_jump_next && strlen($get) > 2) { $another_unread_id = ''; $found_current_get = false; switch ($get[0]) { @@ -276,7 +272,7 @@ class FreshRSS_Context { * @return boolean */ public static function isAutoRemoveAvailable() { - if (!self::$conf->auto_remove_article) { + if (!self::$user_conf->auto_remove_article) { return false; } if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) { @@ -297,7 +293,7 @@ class FreshRSS_Context { * @return boolean */ public static function isStickyPostEnabled() { - if (self::$conf->sticky_post) { + if (self::$user_conf->sticky_post) { return true; } if (self::isAutoRemoveAvailable()) { diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index a39aea327..a384455b4 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -1,6 +1,6 @@ hide_read_feeds && + if (FreshRSS_Context::$user_conf->hide_read_feeds && FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ) && !FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ)) { $class = ' state_unread'; @@ -39,7 +39,7 @@ $feeds = $cat->feeds(); if (!empty($feeds)) { $c_active = FreshRSS_Context::isCurrentGet('c_' . $cat->id()); - $c_show = $c_active && (!FreshRSS_Context::$conf->display_categories || + $c_show = $c_active && (!FreshRSS_Context::$user_conf->display_categories || FreshRSS_Context::$current_get['feed']); ?>
    • @@ -84,7 +84,7 @@
    • - reading_confirm ? 'confirm' : ''; ?> + reading_confirm ? 'confirm' : ''; ?>
    • - queries as $query) { ?> + queries as $query) { ?>
    • - queries) > 0) { ?> + queries) > 0) { ?>
    • @@ -82,7 +82,7 @@ @@ -28,9 +28,9 @@
      @@ -39,9 +39,9 @@
      @@ -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 ffe3c039b..f5c133f07 100644 --- a/app/views/configure/sharing.phtml +++ b/app/views/configure/sharing.phtml @@ -15,8 +15,8 @@
      '> - sharing as $key => $sharing) { ?> - shares[$sharing['type']]; ?> + sharing as $key => $sharing) { ?> + shares[$sharing['type']]; ?>
    Date: Wed, 7 Jan 2015 15:37:24 +0100 Subject: Fix calls to remove_query_by_get() The function has been moved into lib_rss.php See https://github.com/FreshRSS/FreshRSS/issues/730 --- app/Controllers/categoryController.php | 6 ++++-- app/Controllers/feedController.php | 3 ++- lib/lib_rss.php | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php index c90e55ea7..e65c146de 100644 --- a/app/Controllers/categoryController.php +++ b/app/Controllers/categoryController.php @@ -141,7 +141,8 @@ class FreshRSS_category_Controller extends Minz_ActionController { } // Remove related queries. - FreshRSS_Context::$user_conf->remove_query_by_get('c_' . $id); + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'c_' . $id, FreshRSS_Context::$user_conf->queries); FreshRSS_Context::$user_conf->save(); Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect); @@ -177,7 +178,8 @@ class FreshRSS_category_Controller extends Minz_ActionController { // Remove related queries foreach ($feeds as $feed) { - FreshRSS_Context::$user_conf->remove_query_by_get('f_' . $feed->id()); + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $feed->id(), FreshRSS_Context::$user_conf->queries); } FreshRSS_Context::$user_conf->save(); diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index c22669361..c110fda4e 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -477,7 +477,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // TODO: Delete old favicon // Remove related queries - FreshRSS_Context::$user_conf->remove_query_by_get('f_' . $id); + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $id, FreshRSS_Context::$user_conf->queries); FreshRSS_Context::$user_conf->save(); Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url); diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 14b6e854d..ffd56eae4 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -387,3 +387,20 @@ function recursive_unlink($dir) { } return rmdir($dir); } + + +/** + * Remove queries where $get is appearing. + * @param $get the get attribute which should be removed. + * @param $queries an array of queries. + * @return the same array whithout those where $get is appearing. + */ +function remove_query_by_get($get, $queries) { + $final_queries = array(); + foreach ($queries as $key => $query) { + if (empty($query['get']) || $query['get'] !== $get) { + $final_queries[$key] = $query; + } + } + return $final_queries; +} -- cgit v1.2.3 From 9c8a80e57f1e2eb60916eae2e2cbed86d991c750 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 15:39:24 +0100 Subject: Remove .gitkeep in data/users/_/ --- data/users/_/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 data/users/_/.gitkeep diff --git a/data/users/_/.gitkeep b/data/users/_/.gitkeep deleted file mode 100644 index e69de29bb..000000000 -- cgit v1.2.3 From 91e2d4936d71a72d0aefe9736879099283519239 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 15:57:27 +0100 Subject: Add support of configuration_setter A configuration setter must implement only one method: `handle($key, $value)`. Before setting a value in configuration, the setter will be called with this method to check its validity. If a setter has been assigned to a configuration object, it will be called for each of its data so be careful to always return a value (or null if you want to delete the key). See https://github.com/FreshRSS/FreshRSS/issues/730 --- lib/Minz/Configuration.php | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 06a7b43b0..5dbd55876 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -15,9 +15,11 @@ class Minz_Configuration { * @param $namespace the name of the current configuration * @param $config_filename the filename of the configuration * @param $default_filename a filename containing default values for the configuration + * @param $configuration_setter an optional helper to set values in configuration * @throws Minz_ConfigurationNamespaceException if the namespace already exists. */ - public static function register($namespace, $config_filename, $default_filename = null) { + public static function register($namespace, $config_filename, $default_filename = null, + $configuration_setter = null) { if (isset(self::$config_list[$namespace])) { throw new Minz_ConfigurationNamespaceException( $namespace . ' namespace already exists' @@ -25,7 +27,7 @@ class Minz_Configuration { } self::$config_list[$namespace] = new Minz_Configuration( - $namespace, $config_filename, $default_filename + $namespace, $config_filename, $default_filename, $configuration_setter ); } @@ -94,14 +96,21 @@ class Minz_Configuration { */ private $data_default = array(); + /** + * An object which help to set good values in configuration. + */ + private $configuration_setter = null; + /** * Create a new Minz_Configuration object. * * @param $namespace the name of the current configuration. * @param $config_filename the file containing configuration values. * @param $default_filename the file containing default values, null by default. + * @param $configuration_setter an optional helper to set values in configuration */ - private function __construct($namespace, $config_filename, $default_filename = null) { + private function __construct($namespace, $config_filename, $default_filename = null, + $configuration_setter = null) { $this->namespace = $namespace; $this->config_filename = $config_filename; @@ -117,6 +126,8 @@ class Minz_Configuration { if (!is_null($this->default_filename)) { $this->data_default = self::load($this->default_filename); } + + $this->configuration_setter = $configuration_setter; } /** @@ -154,9 +165,13 @@ class Minz_Configuration { * @param $value the value to set. If null, the key is removed from the configuration. */ public function _param($key, $value = null) { + if (!is_null($this->configuration_setter)) { + $value = $this->configuration_setter->handle($key, $value); + } + if (isset($this->data[$key]) && is_null($value)) { unset($this->data[$key]); - } else { + } elseif (!is_null($value)) { $this->data[$key] = $value; } } -- cgit v1.2.3 From 7f12058fab8a455cf33b5df614c5404f7a236d0a Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 16:09:24 +0100 Subject: Add a method to change the configuration setter See https://github.com/FreshRSS/FreshRSS/issues/730 --- lib/Minz/Configuration.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 5dbd55876..019b47fae 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -127,7 +127,18 @@ class Minz_Configuration { $this->data_default = self::load($this->default_filename); } - $this->configuration_setter = $configuration_setter; + $this->_configurationSetter($configuration_setter); + } + + /** + * Set a configuration setter for the current configuration. + * @param $configuration_setter the setter to call when modifying data. It + * must implement an handle($key, $value) method. + */ + public function _configurationSetter($configuration_setter) { + if (is_callable(array($configuration_setter, 'handle'))) { + $this->configuration_setter = $configuration_setter; + } } /** -- cgit v1.2.3 From 4c128e05a4b1245525fd3eae7733e022ee805acd Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 16:13:57 +0100 Subject: Reorganize FreshRSS init See https://github.com/FreshRSS/FreshRSS/issues/730 --- app/FreshRSS.php | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 254e35fee..4900528d3 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -6,15 +6,25 @@ class FreshRSS extends Minz_FrontController { Minz_Session::init('FreshRSS'); } + $this->initConfiguration(); + $this->initAuth(); + FreshRSS_Context::init(); + $this->initI18n(); + FreshRSS_Share::load(join_path(DATA_PATH, 'shares.php')); + $this->loadStylesAndScripts(); + $this->loadNotifications(); + $this->loadExtensions(); + } + + private function initConfiguration() { $current_user = Minz_Session::param('currentUser', '_'); Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php'), join_path(USERS_PATH, '_', 'config.default.php')); + } - // Need to be called just after session init because it initializes - // current user. + private function initAuth() { FreshRSS_Auth::init(); - if (Minz_Request::isPost() && !is_referer_from_same_domain()) { // Basic protection against XSRF attacks FreshRSS_Auth::removeAccess(); @@ -27,17 +37,6 @@ class FreshRSS extends Minz_FrontController { )) ); } - - // Load context and configuration. - FreshRSS_Context::init(); - - $this->initI18n(); - - FreshRSS_Share::load(join_path(DATA_PATH, 'shares.php')); - - $this->loadStylesAndScripts(); - $this->loadNotifications(); - $this->loadExtensions(); } private function initI18n() { -- cgit v1.2.3 From 2fd8a80878441cf59b1689b1765633694cb320b6 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 16:36:55 +0100 Subject: Add first test for a generic ConfigurationSetter We are blocked if a setter has to update several values. ConfigurationSetter will be updated. See https://github.com/FreshRSS/FreshRSS/issues/730 --- app/FreshRSS.php | 7 ++++++- app/Models/ConfigurationSetter.php | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 app/Models/ConfigurationSetter.php diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 4900528d3..002a70af5 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -17,10 +17,15 @@ class FreshRSS extends Minz_FrontController { } private function initConfiguration() { + $configuration_setter = new FreshRSS_ConfigurationSetter(); $current_user = Minz_Session::param('currentUser', '_'); + Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php'), - join_path(USERS_PATH, '_', 'config.default.php')); + join_path(USERS_PATH, '_', 'config.default.php'), + $configuration_setter); + $system_conf = Minz_Configuration::get('system'); + $system_conf->_configurationSetter($configuration_setter); } private function initAuth() { diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php new file mode 100644 index 000000000..e30cb0187 --- /dev/null +++ b/app/Models/ConfigurationSetter.php @@ -0,0 +1,37 @@ + '_language', + 'posts_per_page' => '_posts_per_page', + 'view_mode' => '_view_mode', + ); + + public function handle($key, $value) { + if (isset($this->setters[$key])) { + $value = call_user_func(array($this, $this->setters[$key]), $value); + } + return $value; + } + + private function _language($value) { + $languages = Minz_Translate::availableLanguages(); + if (!isset($languages[$value])) { + $value = 'en'; + } + + return $value; + } + + private function _posts_per_page($value) { + $value = intval($value); + return $value > 0 ? $value : 10; + } + + private function _view_mode($value) { + if (!in_array($value, array('global', 'normal', 'reader'))) { + $value = 'normal'; + } + return $value; + } +} -- cgit v1.2.3 From fb614ab80cf038416e5451b4f6f25f421d75d8e4 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 17:36:29 +0100 Subject: Change way to call configuration setter. - Add a support($key) method which return if the given key is supported by the setter. - Change handle signature by adding a $data param which must be passed by reference. See https://github.com/FreshRSS/FreshRSS/issues/730 --- app/Models/ConfigurationSetter.php | 44 +++++++++++++++++++++++--------------- lib/Minz/Configuration.php | 8 +++---- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index e30cb0187..801e11625 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -1,37 +1,47 @@ '_language', - 'posts_per_page' => '_posts_per_page', - 'view_mode' => '_view_mode', - ); + /** + * Return if the given key is supported by this setter. + * @param $key the key to test. + * @return true if the key is supported, false else. + */ + public function support($key) { + $name_setter = '_' . $key; + return is_callable(array($this, $name_setter)); + } - public function handle($key, $value) { - if (isset($this->setters[$key])) { - $value = call_user_func(array($this, $this->setters[$key]), $value); - } - return $value; + /** + * Set the given key in data with the current value. + * @param $data an array containing the list of all configuration data. + * @param $key the key to update. + * @param $value the value to set. + */ + public function handle(&$data, $key, $value) { + $name_setter = '_' . $key; + call_user_func_array(array($this, $name_setter), array(&$data, $value)); } - private function _language($value) { + /** + * The (long) list of setters. + */ + private function _language(&$data, $value) { $languages = Minz_Translate::availableLanguages(); if (!isset($languages[$value])) { $value = 'en'; } - - return $value; + $data['language'] = $value; } - private function _posts_per_page($value) { + private function _posts_per_page(&$data, $value) { $value = intval($value); - return $value > 0 ? $value : 10; + $data['posts_per_page'] = $value > 0 ? $value : 10; } - private function _view_mode($value) { + private function _view_mode(&$data, $value) { if (!in_array($value, array('global', 'normal', 'reader'))) { $value = 'normal'; } - return $value; + $data['view_mode'] = $value; } } diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 019b47fae..6044fc269 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -176,11 +176,9 @@ class Minz_Configuration { * @param $value the value to set. If null, the key is removed from the configuration. */ public function _param($key, $value = null) { - if (!is_null($this->configuration_setter)) { - $value = $this->configuration_setter->handle($key, $value); - } - - if (isset($this->data[$key]) && is_null($value)) { + if (!is_null($this->configuration_setter) && $this->configuration_setter->support($key)) { + $this->configuration_setter->handle($this->data, $key, $value); + } elseif (isset($this->data[$key]) && is_null($value)) { unset($this->data[$key]); } elseif (!is_null($value)) { $this->data[$key] = $value; -- cgit v1.2.3 From 7e81e67aeda7de923f31bfa36f6fcfa41f34dc9b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 18:16:57 +0100 Subject: Add setters for the user configurations See https://github.com/FreshRSS/FreshRSS/issues/730 --- app/Models/ConfigurationSetter.php | 197 +++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 801e11625..64ca61eee 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -25,6 +25,47 @@ class FreshRSS_ConfigurationSetter { /** * The (long) list of setters. */ + private function _apiPasswordHash(&$data, $value) { + $data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; + } + + private function _content_width(&$data, $value) { + if (!in_array($value, array('thin', 'medium', 'large', 'no_limit'))) { + $value = 'thin'; + } + + $data['content_width'] = $value; + } + + private function _default_state(&$data, $value) { + $data['default_state'] = (int)$value; + } + + private function _default_view(&$data, $value) { + switch ($value) { + case 'all': + $data['default_view'] = $value; + $data['default_state'] = (FreshRSS_Entry::STATE_READ + + FreshRSS_Entry::STATE_NOT_READ); + break; + case 'adaptive': + case 'unread': + default: + $data['default_view'] = $value; + $data['default_state'] = FreshRSS_Entry::STATE_NOT_READ; + } + } + + private function _html5_notif_timeout(&$data, $value) { + $value = intval($value); + $data['html5_notif_timeout'] = $value >= 0 ? $value : 0; + } + + private function _keep_history_default(&$data, $value) { + $value = intval($value); + $data['keep_history_default'] = $value >= -1 ? $value : 0; + } + private function _language(&$data, $value) { $languages = Minz_Translate::availableLanguages(); if (!isset($languages[$value])) { @@ -33,15 +74,171 @@ class FreshRSS_ConfigurationSetter { $data['language'] = $value; } + private function _mail_login(&$data, $value) { + $value = filter_var($value, FILTER_VALIDATE_EMAIL); + $data['mail_login'] = $value ? $value : ''; + } + + private function _old_entries(&$data, $value) { + $value = intval($value); + $data['old_entries'] = $value > 0 ? $value : 3; + } + + private function _passwordHash(&$data, $value) { + $data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; + } + private function _posts_per_page(&$data, $value) { $value = intval($value); $data['posts_per_page'] = $value > 0 ? $value : 10; } + 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; + } + } + + private function _sharing(&$data, $values) { + $data['sharing'] = array(); + foreach ($values as $value) { + if (!is_array($value)) { + continue; + } + + // Verify URL and add default value when needed + if (isset($value['url'])) { + $is_url = ( + filter_var($value['url'], FILTER_VALIDATE_URL) || + (version_compare(PHP_VERSION, '5.3.3', '<') && + (strpos($value, '-') > 0) && + ($value === filter_var($value, FILTER_SANITIZE_URL))) + ); //PHP bug #51192 + if (!$is_url) { + continue; + } + } else { + $value['url'] = null; + } + + $data['sharing'][] = $value; + } + } + + private function _shortcuts(&$data, $values) { + foreach ($values as $key => $value) { + if (isset($data['shortcuts'][$key])) { + $data['shortcuts'][$key] = $value; + } + } + } + + private function _sort_order(&$data, $value) { + $data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC'; + } + + private function _ttl_default(&$data, $value) { + $value = intval($value); + $data['ttl_default'] = $value >= -1 ? $value : 3600; + } + private function _view_mode(&$data, $value) { if (!in_array($value, array('global', 'normal', 'reader'))) { $value = 'normal'; } $data['view_mode'] = $value; } + + /** + * A list of boolean setters. + */ + private function handleBool($value) { + return ((bool)$value) && $value !== 'no'; + } + + private function _anon_access(&$data, $value) { + $data['anon_access'] = $this->handleBool($value); + } + + private function _auto_load_more(&$data, $value) { + $data['auto_load_more'] = $this->handleBool($value); + } + + private function _auto_remove_article(&$data, $value) { + $data['auto_remove_article'] = $this->handleBool($value); + } + + private function _display_categories(&$data, $value) { + $data['display_categories'] = $this->handleBool($value); + } + + private function _display_posts(&$data, $value) { + $data['display_posts'] = $this->handleBool($value); + } + + private function _hide_read_feeds(&$data, $value) { + $data['hide_read_feeds'] = $this->handleBool($value); + } + + private function _lazyload(&$data, $value) { + $data['lazyload'] = $this->handleBool($value); + } + + private function _mark_when(&$data, $values) { + foreach ($values as $key => $value) { + if (isset($data['mark_when'][$key])) { + $data['mark_when'][$key] = $this->handleBool($value); + } + } + } + + private function _onread_jump_next(&$data, $value) { + $data['onread_jump_next'] = $this->handleBool($value); + } + + private function _reading_confirm(&$data, $value) { + $data['reading_confirm'] = $this->handleBool($value); + } + + private function _sticky_post(&$data, $value) { + $data['sticky_post'] = $this->handleBool($value); + } + + private function _bottomline_date(&$data, $value) { + $data['bottomline_date'] = $this->handleBool($value); + } + private function _bottomline_favorite(&$data, $value) { + $data['bottomline_favorite'] = $this->handleBool($value); + } + private function _bottomline_link(&$data, $value) { + $data['bottomline_link'] = $this->handleBool($value); + } + private function _bottomline_read(&$data, $value) { + $data['bottomline_read'] = $this->handleBool($value); + } + private function _bottomline_sharing(&$data, $value) { + $data['bottomline_sharing'] = $this->handleBool($value); + } + private function _bottomline_tags(&$data, $value) { + $data['bottomline_tags'] = $this->handleBool($value); + } + + private function _topline_date(&$data, $value) { + $data['topline_date'] = $this->handleBool($value); + } + private function _topline_favorite(&$data, $value) { + $data['topline_favorite'] = $this->handleBool($value); + } + private function _topline_link(&$data, $value) { + $data['topline_link'] = $this->handleBool($value); + } + private function _topline_read(&$data, $value) { + $data['topline_read'] = $this->handleBool($value); + } } -- cgit v1.2.3 From 9265cd57333b2a91effc6ea284b504fbc977b9ed Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 7 Jan 2015 18:55:18 +0100 Subject: Add system config setter methods See https://github.com/FreshRSS/FreshRSS/issues/730 --- app/Models/ConfigurationSetter.php | 134 +++++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 5 deletions(-) diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 64ca61eee..9830fed28 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -23,13 +23,24 @@ class FreshRSS_ConfigurationSetter { } /** - * The (long) list of setters. + * A helper to set boolean values. + * + * @param $value the tested value. + * @return true if value is true and different from no, false else. + */ + private function handleBool($value) { + return ((bool)$value) && $value !== 'no'; + } + + /** + * The (long) list of setters for user configuration. */ private function _apiPasswordHash(&$data, $value) { $data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; } private function _content_width(&$data, $value) { + $value = strtolower($value); if (!in_array($value, array('thin', 'medium', 'large', 'no_limit'))) { $value = 'thin'; } @@ -66,7 +77,9 @@ class FreshRSS_ConfigurationSetter { $data['keep_history_default'] = $value >= -1 ? $value : 0; } + // It works for system config too! private function _language(&$data, $value) { + $value = strtolower($value); $languages = Minz_Translate::availableLanguages(); if (!isset($languages[$value])) { $value = 'en'; @@ -149,6 +162,7 @@ class FreshRSS_ConfigurationSetter { } private function _view_mode(&$data, $value) { + $value = strtolower($value); if (!in_array($value, array('global', 'normal', 'reader'))) { $value = 'normal'; } @@ -158,10 +172,6 @@ class FreshRSS_ConfigurationSetter { /** * A list of boolean setters. */ - private function handleBool($value) { - return ((bool)$value) && $value !== 'no'; - } - private function _anon_access(&$data, $value) { $data['anon_access'] = $this->handleBool($value); } @@ -241,4 +251,118 @@ class FreshRSS_ConfigurationSetter { private function _topline_read(&$data, $value) { $data['topline_read'] = $this->handleBool($value); } + + /** + * The (not so long) list of setters for system configuration. + */ + private function _allow_anonymous(&$data, $value) { + $data['allow_anonymous'] = $this->handleBool($value) && FreshRSS_Auth::accessNeedsAction(); + } + + private function _allow_anonymous_refresh(&$data, $value) { + $data['allow_anonymous_refresh'] = $this->handleBool($value) && $data['allow_anonymous']; + } + + private function _api_enabled(&$data, $value) { + $data['api_enabled'] = $this->handleBool($value); + } + + private function _auth_type(&$data, $value) { + $value = strtolower($value); + if (!in_array($value, array('form', 'http_auth', 'persona', 'none'))) { + $value = 'none'; + } + $data['auth_type'] = $value; + $this->_allow_anonymous($data, $data['allow_anonymous']); + } + + private function _db(&$data, $value) { + if (!isset($value['type'])) { + return; + } + + switch ($value['type']) { + case 'mysql': + if (empty($value['host']) || + empty($value['user']) || + empty($value['base']) || + !isset($value['password'])) { + return; + } + + $data['db']['type'] = $value['type']; + $data['db']['host'] = $value['host']; + $data['db']['user'] = $value['user']; + $data['db']['base'] = $value['base']; + $data['db']['password'] = $value['password']; + $data['db']['prefix'] = isset($value['prefix']) ? $value['prefix'] : ''; + break; + case 'sqlite': + $data['db']['type'] = $value['type']; + $data['db']['host'] = ''; + $data['db']['user'] = ''; + $data['db']['base'] = ''; + $data['db']['password'] = ''; + $data['db']['prefix'] = ''; + break; + default: + return; + } + } + + private function _default_user(&$data, $value) { + $user_list = listUsers(); + if (in_array($value, $user_list)) { + $data['default_user'] = $value; + } + } + + private function _environment(&$data, $value) { + $value = strtolower($value); + if (!in_array($value, array('silent', 'development', 'production'))) { + $value = 'production'; + } + $data['environment'] = $value; + } + + private function _limits(&$data, $values) { + $max_small_int = 16384; + $limits_keys = array( + 'cache_duration' => array( + 'min' => 0, + ), + 'timeout' => array( + 'min' => 0, + ), + 'max_inactivity' => array( + 'min' => 0, + ), + 'max_feeds' => array( + 'min' => 0, + 'max' => $max_small_int, + ), + 'max_categories' => array( + 'min' => 0, + 'max' => $max_small_int, + ), + ); + + foreach ($values as $key => $value) { + if (!isset($limits_keys[$key])) { + continue; + } + + $limits = $limits_keys[$key]; + if ( + (!isset($limits['min']) || $value > $limits['min']) && + (!isset($limits['max']) || $value < $limits['max']) + ) { + $data['limits'][$key] = $value; + } + } + } + + private function _unsafe_autologin_enabled(&$data, $value) { + $data['unsafe_autologin_enabled'] = $this->handleBool($value); + } } -- cgit v1.2.3 From 59abb0c7540369e59b9b81caa867fc7b70c52667 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 12:12:41 +0100 Subject: Fix i18n in install.php --- app/install.php | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/app/install.php b/app/install.php index 8b65fbb6f..a2c12e79f 100644 --- a/app/install.php +++ b/app/install.php @@ -43,30 +43,29 @@ function param($key, $default = false) { // gestion internationalisation function initTranslate() { + $available_languages = array( + 'en' => 'English', + 'fr' => 'Français' + ); + if (!isset($_SESSION['language'])) { - $_SESSION['language'] = getBetterLanguage('en'); + $best = get_best_language(); + if (!isset($available_languages[$best])) { + $best = 'en'; + } + + $_SESSION['language'] = $best; } - Minz_Translate::init(); + Minz_Translate::init(array( + 'en' => 'English', + 'fr' => 'Français', + ), $_SESSION['language']); } -function getBetterLanguage($fallback) { - $available = availableLanguages(); +function get_best_language() { $accept = $_SERVER['HTTP_ACCEPT_LANGUAGE']; - $language = strtolower(substr($accept, 0, 2)); - - if (isset($available[$language])) { - return $language; - } else { - return $fallback; - } -} - -function availableLanguages() { - return array( - 'en' => 'English', - 'fr' => 'Français' - ); + return strtolower(substr($accept, 0, 2)); } @@ -263,7 +262,7 @@ function checkStep() { } function checkStep0() { - $languages = availableLanguages(); + $languages = Minz_Translate::availableLanguages(); $language = isset($_SESSION['language']) && isset($languages[$_SESSION['language']]); @@ -427,7 +426,8 @@ function checkBD() { /*** AFFICHAGE ***/ function printStep0() { - global $actual; + $actual = Minz_Translate::language(); + $languages = Minz_Translate::availableLanguages(); ?>

    @@ -439,7 +439,6 @@ function printStep0() {
    - $lib) { ?> - + +
    diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index b05c9c695..d870b6bb2 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -9,6 +9,11 @@ * It uses files in `./app/i18n/` */ class Minz_Translate { + /** + * $path_list is the list of registered base path to search translations. + */ + private static $path_list = array(); + /** * $lang_name is the name of the current language to use. */ @@ -30,6 +35,7 @@ class Minz_Translate { */ public static function init($lang_name) { self::$lang_name = $lang_name; + self::$path_list = array(); self::$lang_files = array(); self::$translates = array(); self::registerPath(APP_PATH . '/i18n'); @@ -45,11 +51,21 @@ class Minz_Translate { /** * Return the list of available languages. - * @return an array. - * @todo fix this method. + * @return an array containing langs found in different registered paths. */ public static function availableLanguages() { - return array(); + $list_langs = array(); + + foreach (self::$path_list as $path) { + $path_langs = array_values(array_diff( + scandir($path), + array('..', '.') + )); + + $list_langs = array_merge($list_langs, $path_langs); + } + + return array_unique($list_langs); } /** @@ -58,6 +74,12 @@ class Minz_Translate { * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). */ public static function registerPath($path) { + if (in_array($path, self::$path_list)) { + return; + } + + self::$path_list[] = $path; + // We load first i18n files for the current language. $lang_path = $path . '/' . self::$lang_name; $list_i18n_files = array_values(array_diff( -- cgit v1.2.3 From 8f04cb9d9db90cce71a0de81bcfbc68b0873ea23 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 15:05:25 +0100 Subject: Change Minz_Translate::reset() behaviour - Don't reset the path list (use init() instead) - init() accept a null lang_name. To use i18n, you'll have to use reset() later. It is helpful to load the list of available language before choosing one of them. --- lib/Minz/Translate.php | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index d870b6bb2..a90af659e 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -33,7 +33,7 @@ class Minz_Translate { * Init the translation object. * @param $lang_name the lang to show. */ - public static function init($lang_name) { + public static function init($lang_name = null) { self::$lang_name = $lang_name; self::$path_list = array(); self::$lang_files = array(); @@ -46,7 +46,12 @@ class Minz_Translate { * @param $lang_name the new language to use */ public static function reset($lang_name) { - self::init($lang_name); + self::$lang_name = $lang_name; + self::$lang_files = array(); + self::$translates = array(); + foreach ($path_list as $path) { + self::loadLang($path); + } } /** @@ -70,7 +75,6 @@ class Minz_Translate { /** * Register a new path and load i18n files inside. - * * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). */ public static function registerPath($path) { @@ -79,9 +83,20 @@ class Minz_Translate { } self::$path_list[] = $path; + self::loadLang($path); + } - // We load first i18n files for the current language. + /** + * Load translations of the current language from the given path. + * @param $path the path containing i18n directories. + */ + private static function loadLang($path) { $lang_path = $path . '/' . self::$lang_name; + if (!file_exists($lang_path) || is_null(self::$lang_name)) { + // The lang path does not exist, nothing more to do. + return; + } + $list_i18n_files = array_values(array_diff( scandir($lang_path), array('..', '.') @@ -102,7 +117,6 @@ class Minz_Translate { /** * Load the files associated to $key into $translates. - * * @param $key the top level i18n key we want to load. */ private static function loadKey($key) { -- cgit v1.2.3 From 85ea5e548ac1057feeb8dfff99b1b433e4ecfd6b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 15:16:32 +0100 Subject: Fix install.php script (choice of lang) Fix a bug in Minz_Translate too --- app/install.php | 25 ++++++++++++------------- lib/Minz/Translate.php | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/install.php b/app/install.php index 55817ec68..a79b0d318 100644 --- a/app/install.php +++ b/app/install.php @@ -43,21 +43,18 @@ function param($key, $default = false) { // gestion internationalisation function initTranslate() { - $available_languages = array( - 'en' => 'English', - 'fr' => 'Français' - ); + Minz_Translate::init(); + $available_languages = Minz_Translate::availableLanguages(); if (!isset($_SESSION['language'])) { - $best = get_best_language(); - if (!isset($available_languages[$best])) { - $best = 'en'; - } + $_SESSION['language'] = get_best_language(); + } - $_SESSION['language'] = $best; + if (!in_array($_SESSION['language'], $available_languages)) { + $_SESSION['language'] = 'en'; } - Minz_Translate::init($_SESSION['language']); + Minz_Translate::reset($_SESSION['language']); } function get_best_language() { @@ -254,7 +251,7 @@ function checkStep() { function checkStep0() { $languages = Minz_Translate::availableLanguages(); $language = isset($_SESSION['language']) && - isset($languages[$_SESSION['language']]); + in_array($_SESSION['language'], $languages); return array( 'language' => $language ? 'ok' : 'ko', @@ -429,8 +426,10 @@ function printStep0() {
    diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index a90af659e..24497a193 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -49,7 +49,7 @@ class Minz_Translate { self::$lang_name = $lang_name; self::$lang_files = array(); self::$translates = array(); - foreach ($path_list as $path) { + foreach (self::$path_list as $path) { self::loadLang($path); } } -- cgit v1.2.3 From 19dfef8b49f78e898ea5841869ff80cc351724bc Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 16:32:13 +0100 Subject: Fix bug with Minz_Translate When a path was registered before initialization, it was not considered unless by calling reset() method. This is fixed now. --- app/views/user/manage.phtml | 4 ++-- lib/Minz/Translate.php | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml index 11c42a857..466446f2f 100644 --- a/app/views/user/manage.phtml +++ b/app/views/user/manage.phtml @@ -11,8 +11,8 @@
    diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index 24497a193..d8ce2a0f7 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -35,10 +35,12 @@ class Minz_Translate { */ public static function init($lang_name = null) { self::$lang_name = $lang_name; - self::$path_list = array(); self::$lang_files = array(); self::$translates = array(); self::registerPath(APP_PATH . '/i18n'); + foreach (self::$path_list as $path) { + self::loadLang($path); + } } /** @@ -74,7 +76,7 @@ class Minz_Translate { } /** - * Register a new path and load i18n files inside. + * Register a new path. * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). */ public static function registerPath($path) { @@ -83,7 +85,6 @@ class Minz_Translate { } self::$path_list[] = $path; - self::loadLang($path); } /** @@ -164,7 +165,8 @@ class Minz_Translate { // If $translates[$top_level] is null it means we have to load the // corresponding files. - if (is_null(self::$translates[$top_level])) { + if (!isset(self::$translates[$top_level]) || + is_null(self::$translates[$top_level])) { $res = self::loadKey($top_level); if (!$res) { return $key; -- cgit v1.2.3 From b23fc3187cb90800aad6417badf7822a8d280b74 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 21:32:21 +0100 Subject: Fix translation bug A path registered after initialization must be loaded. --- app/i18n/en/admin.php | 1 + app/i18n/fr/admin.php | 1 + lib/Minz/Translate.php | 1 + 3 files changed, 3 insertions(+) diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index 83c06bfb4..6a89d4a31 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -104,6 +104,7 @@ return array( ), 'extensions' => array( 'empty_list' => 'There is no installed extension', + 'no_configure_view' => 'This extension cannot be configured.', 'system' => 'System extension (you have no rights on it)', 'title' => 'Extensions', ), diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index b16e2be54..2e879da61 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -104,6 +104,7 @@ return array( ), 'extensions' => array( 'empty_list' => 'Il n’y a aucune extension installée.', + 'no_configure_view' => 'Cette extension ne peut pas être configurée.', 'system' => 'Extension système (vous n’avez aucun droit dessus)', 'title' => 'Extensions', ), diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index d8ce2a0f7..baddcb424 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -85,6 +85,7 @@ class Minz_Translate { } self::$path_list[] = $path; + self::loadLang($path); } /** -- cgit v1.2.3 From cb8d1480c1e39c27a96939f7ed51a60f33a7e62b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 21:34:29 +0100 Subject: Update extensions README --- extensions/README.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/extensions/README.md b/extensions/README.md index e7b66d5bc..4c1a81f5c 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -1,15 +1,3 @@ -== FreshRSS extensions == +# FreshRSS extensions You may place in this directory some custom extensions for FreshRSS. - -The structure must be: - -./FreshRSS/extensions/ - ./NameOfExtensionAlphanumeric/ - ./style.css - ./script.js - ./module.php - -Each file is optional. - -The name of non-official extensions should start by an 'x'. -- cgit v1.2.3 From 03db0b5d3c431deb8c4f099b77e045160548f1b6 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 21:37:59 +0100 Subject: Fix a bug related to sharing $item and $feed were not existing, replaced by other names. Introduce by the merging of dev in extension branch. --- app/views/helpers/index/normal/entry_bottom.phtml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml index 32317d027..20b4b332c 100644 --- a/app/views/helpers/index/normal/entry_bottom.phtml +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -36,16 +36,16 @@
  • -- cgit v1.2.3 From 0745252b68f6f9b7c91ea437893b5f33b7a224c3 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 10 May 2015 20:31:03 +0200 Subject: Hexadecimal literals do not work with SQLite/PDO X'09AF' hexadecimal literals do not work with SQLite/PDO. Replaced by PHP hex2bin(). https://github.com/FreshRSS/FreshRSS/commit/711530a512b370d79b079205ce1f8376174f7f03 --- app/Models/EntryDAO.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index ebaeb3868..172eac897 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -54,7 +54,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) ' . 'VALUES(?, ?, ?, ?, ' . ($this->isCompressed() ? 'COMPRESS(?)' : '?') - . ', ?, ?, ?, X?, ?, ?, ?, ?)'; + . ', ?, ?, ?, ?, ?, ?, ?, ?)'; $this->addEntryPrepared = $this->bd->prepare($sql); } @@ -67,7 +67,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], time(), - $valuesTmp['hash'], + hex2bin($valuesTmp['hash']), // X'09AF' hexadecimal literals do not work with SQLite/PDO $valuesTmp['is_read'] ? 1 : 0, $valuesTmp['is_favorite'] ? 1 : 0, $valuesTmp['id_feed'], @@ -77,7 +77,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($this->addEntryPrepared && $this->addEntryPrepared->execute($values)) { return $this->bd->lastInsertId(); } else { - $info = $this->addEntryPrepared == null ? array(2 => 'syntax error') : $this->addEntryPrepared->errorInfo(); + $info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo(); if ($this->autoAddColumn($info)) { return $this->addEntry($valuesTmp); } elseif ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries @@ -99,7 +99,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $sql = 'UPDATE `' . $this->prefix . 'entry` ' . 'SET title=?, author=?, ' . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?') - . ', link=?, date=?, lastSeen=?, hash=X?, ' + . ', link=?, date=?, lastSeen=?, hash=?, ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ') . 'tags=? ' . 'WHERE id_feed=? AND guid=?'; @@ -113,7 +113,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { substr($valuesTmp['link'], 0, 1023), $valuesTmp['date'], time(), - $valuesTmp['hash'], + hex2bin($valuesTmp['hash']), ); if ($valuesTmp['is_read'] !== null) { $values[] = $valuesTmp['is_read'] ? 1 : 0; @@ -127,7 +127,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) { return $this->bd->lastInsertId(); } else { - $info = $this->updateEntryPrepared == null ? array(2 => 'syntax error') : $this->updateEntryPrepared->errorInfo(); + $info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo(); if ($this->autoAddColumn($info)) { return $this->updateEntry($valuesTmp); } @@ -598,7 +598,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $result; } else { - $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); if ($this->autoAddColumn($info)) { return $this->listHashForFeedGuids($id_feed, $guids); } @@ -619,7 +619,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); if ($this->autoAddColumn($info)) { return $this->updateLastSeen($id_feed, $guids); } -- cgit v1.2.3 From ead849dbe3717966dff50e55d6bf849dafde87b7 Mon Sep 17 00:00:00 2001 From: Tets Date: Mon, 11 May 2015 10:20:17 +0200 Subject: 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', -- cgit v1.2.3 From 217c191a1ba3ac03b847d261a32e19975380fcad Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Mon, 11 May 2015 22:42:41 +0200 Subject: More SQLite compatibility Additional changes to add compatibility with SQLite for the new hash/lastSeen mode of updating articles. --- app/Models/EntryDAO.php | 71 ++++++++++++++++++++++++------------------ app/Models/EntryDAOSQLite.php | 15 +++++++++ app/Models/FeedDAO.php | 11 ++++--- app/SQL/install.sql.mysql.php | 2 +- app/SQL/install.sql.sqlite.php | 2 +- app/install.php | 2 ++ lib/Minz/ModelPdo.php | 5 +++ 7 files changed, 70 insertions(+), 38 deletions(-) diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 172eac897..eae9683ad 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -6,38 +6,48 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return parent::$sharedDbType !== 'sqlite'; } - protected function autoAddColumn($errorInfo) { - if (isset($errorInfo[0])) { - if ($errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR - $hasTransaction = false; - try { - $stm = null; - if (stripos($errorInfo[2], 'lastSeen') !== false) { //v1.2 - if (!$this->bd->inTransaction()) { - $this->bd->beginTransaction(); - $hasTransaction = true; - } - $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN lastSeen INT(11) NOT NULL'); - if ($stm && $stm->execute()) { - $stm = $this->bd->prepare('CREATE INDEX entry_lastSeen_index ON `' . $this->prefix . 'entry`(`lastSeen`);'); //"IF NOT EXISTS" does not exist in MySQL 5.7 - if ($stm && $stm->execute()) { - if ($hasTransaction) { - $this->bd->commit(); - } - return true; - } - } + protected function addColumn($name) { + Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn: ' . $name); + $hasTransaction = false; + try { + $stm = null; + if ($name === 'lastSeen') { //v1.2 + if (!$this->bd->inTransaction()) { + $this->bd->beginTransaction(); + $hasTransaction = true; + } + $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN lastSeen INT(11) DEFAULT 0'); + if ($stm && $stm->execute()) { + $stm = $this->bd->prepare('CREATE INDEX entry_lastSeen_index ON `' . $this->prefix . 'entry`(`lastSeen`);'); //"IF NOT EXISTS" does not exist in MySQL 5.7 + if ($stm && $stm->execute()) { if ($hasTransaction) { - $this->bd->rollBack(); + $this->bd->commit(); } - } elseif (stripos($errorInfo[2], 'hash') !== false) { //v1.2 - $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN hash BINARY(16) NOT NULL'); - return $stm && $stm->execute(); + return true; } - } catch (Exception $e) { - Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn error: ' . $e->getMessage()); - if ($hasTransaction) { - $this->bd->rollBack(); + } + if ($hasTransaction) { + $this->bd->rollBack(); + } + } elseif ($name === 'hash') { //v1.2 + $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN hash BINARY(16)'); + return $stm && $stm->execute(); + } + } catch (Exception $e) { + Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn error: ' . $e->getMessage()); + if ($hasTransaction) { + $this->bd->rollBack(); + } + } + return false; + } + + protected function autoAddColumn($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR + foreach (array('lastSeen', 'hash') as $column) { + if (stripos($errorInfo[2], $column) !== false) { + return $this->addColumn($column); } } } @@ -82,7 +92,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $this->addEntry($valuesTmp); } elseif ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] - . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']); + . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']. ' ' . $this->addEntryPrepared); } return false; } @@ -597,7 +607,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } return $result; } else { - $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); if ($this->autoAddColumn($info)) { return $this->listHashForFeedGuids($id_feed, $guids); diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index ffe0f037c..ff049d813 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -2,6 +2,21 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { + protected function autoAddColumn($errorInfo) { + if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR + if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) { + $showCreate = $tableInfo->fetchColumn(); + Minz_Log::debug('FreshRSS_EntryDAOSQLite::autoAddColumn: ' . $showCreate); + foreach (array('lastSeen', 'hash') as $column) { + if (stripos($showCreate, $column) === false) { + return $this->addColumn($column); + } + } + } + } + return false; + } + protected function sqlConcat($s1, $s2) { return $s1 . '||' . $s2; } diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 76025ff53..475d39286 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -330,11 +330,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery' $stm = $this->bd->prepare($sql); - $id_max = intval($date_min) . '000000'; - - $stm->bindParam(':id_feed', $id, PDO::PARAM_INT); - $stm->bindParam(':id_max', $id_max, PDO::PARAM_STR); - $stm->bindParam(':keep', $keep, PDO::PARAM_INT); + if ($stm) { + $id_max = intval($date_min) . '000000'; + $stm->bindParam(':id_feed', $id, PDO::PARAM_INT); + $stm->bindParam(':id_max', $id_max, PDO::PARAM_STR); + $stm->bindParam(':keep', $keep, PDO::PARAM_INT); + } if ($stm && $stm->execute()) { return $stm->rowCount(); diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index afdd821b2..9c6af405d 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` ( `content_bin` blob, -- v0.7 `link` varchar(1023) CHARACTER SET latin1 NOT NULL, `date` int(11), -- Until year 2038 - `lastSeen` INT(11) NOT NULL, -- v1.2, Until year 2038 + `lastSeen` INT(11) DEFAULT 0, -- v1.2, Until year 2038 `hash` BINARY(16), -- v1.2 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index 7517ead45..77e8e094c 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -39,7 +39,7 @@ $SQL_CREATE_TABLES = array( `content` text, `link` varchar(1023) NOT NULL, `date` int(11), -- Until year 2038 - `lastSeen` INT(11) NOT NULL, -- v1.2, Until year 2038 + `lastSeen` INT(11) DEFAULT 0, -- v1.2, Until year 2038 `hash` BINARY(16), -- v1.2 `is_read` boolean NOT NULL DEFAULT 0, `is_favorite` boolean NOT NULL DEFAULT 0, diff --git a/app/install.php b/app/install.php index 177173fdb..86afb9318 100644 --- a/app/install.php +++ b/app/install.php @@ -168,8 +168,10 @@ function saveStep3() { $_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] .(empty($_SESSION['default_user']) ? '' :($_SESSION['default_user'] . '_')); } + //TODO: load `config.default.php` as default $config_array = array( 'environment' => 'production', + 'simplepie_syslog_enabled' => true, 'salt' => $_SESSION['salt'], 'title' => $_SESSION['title'], 'default_user' => $_SESSION['default_user'], diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index ac7a1bed7..3e8ec1f43 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -134,4 +134,9 @@ class MinzPDO extends PDO { MinzPDO::check($statement); return parent::exec($statement); } + + public function query($statement) { + MinzPDO::check($statement); + return parent::query($statement); + } } -- cgit v1.2.3 From 256c8613a4046931dcd28ab22b6aebe8752a98c2 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 15 May 2015 03:21:36 +0200 Subject: First draft of PubSubHubbub https://github.com/FreshRSS/FreshRSS/issues/312 Requires setting base_url in config.php. Currently using the filesystem (no change to the database) --- app/Controllers/feedController.php | 55 +++++++++++------ app/Models/Feed.php | 69 ++++++++++++++++++++- constants.php | 1 + data/PubSubHubbub/feeds/.gitignore | 1 + data/PubSubHubbub/feeds/README.md | 12 ++++ data/PubSubHubbub/secrets/.gitignore | 1 + data/PubSubHubbub/secrets/README.md | 4 ++ data/config.default.php | 8 ++- lib/lib_rss.php | 9 +++ p/api/pshb.php | 116 +++++++++++++++++++++++++++++++++++ 10 files changed, 252 insertions(+), 24 deletions(-) create mode 100644 data/PubSubHubbub/feeds/.gitignore create mode 100644 data/PubSubHubbub/feeds/README.md create mode 100644 data/PubSubHubbub/secrets/.gitignore create mode 100644 data/PubSubHubbub/secrets/README.md create mode 100644 p/api/pshb.php diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 0443b4159..9117da639 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -168,6 +168,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Ok, feed has been added in database. Now we have to refresh entries. $feed->_id($id); $feed->faviconPrepare(); + $feed->pubSubHubbubPrepare(); $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; @@ -261,12 +262,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { * This action actualizes entries from one or several feeds. * * Parameters are: - * - id (default: false) + * - id (default: false): Feed ID + * - url (default: false): Feed URL * - force (default: false) - * If id is not specified, all the feeds are actualized. But if force is + * If id and url are not specified, all the feeds are actualized. But if force is * false, process stops at 10 feeds to avoid time execution problem. */ - public function actualizeAction() { + public function actualizeAction($simplePie = null) { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -274,14 +276,15 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Session::_param('actualize_feeds', false); $id = Minz_Request::param('id'); + $url = Minz_Request::param('url'); $force = Minz_Request::param('force'); // Create a list of feeds to actualize. // If id is set and valid, corresponding feed is added to the list but // alone in order to automatize further process. $feeds = array(); - if ($id) { - $feed = $feedDAO->searchById($id); + if ($id || $url) { + $feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url); if ($feed) { $feeds[] = $feed; } @@ -302,8 +305,11 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } try { - // Load entries - $feed->load(false); + if ($simplePie) { + $feed->loadEntries($simplePie); //Used by PubSubHubbub + } else { + $feed->load(false); + } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::notice($e->getMessage()); $feedDAO->updateLastUpdate($feed->id(), true); @@ -404,7 +410,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } - $feed->faviconPrepare(); + if ($simplePie === null) { + $feed->faviconPrepare(); + if ($feed->url() === 'http://push-pub.appspot.com/feed') { + $secret = $feed->pubSubHubbubPrepare(); + if ($secret != '') { + Minz_Log::debug('PubSubHubbub subscribe ' . $feed->url()); + $feed->pubSubHubbubSubscribe(true, $secret); + } + } + } $feed->unlock(); $updated_feeds++; unset($feed); @@ -427,20 +442,20 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Session::_param('notification', $notif); // No layout in ajax request. $this->view->_useLayout(false); - return; - } - - // Redirect to the main page with correct notification. - if ($updated_feeds === 1) { - $feed = reset($feeds); - Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array( - 'params' => array('get' => 'f_' . $feed->id()) - )); - } elseif ($updated_feeds > 1) { - Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array()); } else { - Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array()); + // Redirect to the main page with correct notification. + if ($updated_feeds === 1) { + $feed = reset($feeds); + Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array( + 'params' => array('get' => 'f_' . $feed->id()) + )); + } elseif ($updated_feeds > 1) { + Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array()); + } else { + Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array()); + } } + return $updated_feeds; } /** diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 85fb173ec..dcf083ea8 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -19,6 +19,8 @@ class FreshRSS_Feed extends Minz_Model { private $ttl = -2; private $hash = null; private $lockPath = ''; + private $hubUrl = ''; + private $selfUrl = ''; public function __construct($url, $validate=true) { if ($validate) { @@ -226,6 +228,11 @@ class FreshRSS_Feed extends Minz_Model { throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Feed error' : $errorMessage) . ' [' . $url . ']'); } + $links = $feed->get_links('self'); + $this->selfUrl = isset($links[0]) ? $links[0] : null; + $links = $feed->get_links('hub'); + $this->hubUrl = isset($links[0]) ? $links[0] : null; + if ($loadDetails) { // si on a utilisé l'auto-discover, notre url va avoir changé $subscribe_url = $feed->subscribe_url(false); @@ -259,7 +266,7 @@ class FreshRSS_Feed extends Minz_Model { } } - private function loadEntries($feed) { + public function loadEntries($feed) { $entries = array(); foreach ($feed->get_items() as $item) { @@ -333,4 +340,64 @@ class FreshRSS_Feed extends Minz_Model { function unlock() { @unlink($this->lockPath); } + + // + + function pubSubHubbubPrepare() { + $secret = ''; + if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { + $path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl); + if (!file_exists($path . '/hub.txt')) { + @mkdir($path, 0777, true); + file_put_contents($path . '/hub.txt', $this->hubUrl); + $secret = sha1(FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true)); + file_put_contents($path . '/secret.txt', $secret); + @mkdir(PSHB_PATH . '/secrets/'); + file_put_contents(PSHB_PATH . '/secrets/' . $secret . '.txt', base64url_encode($this->selfUrl)); + Minz_Log::notice('PubSubHubbub prepared for ' . $this->url); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . + 'PubSubHubbub prepared for ' . $this->url . "\n", FILE_APPEND); + } + $path .= '/' . base64url_encode($this->url); + $currentUser = Minz_Session::param('currentUser'); + if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) { + @mkdir($path, 0777, true); + touch($path . '/' . $currentUser . '.txt'); + } + } + return $secret; + } + + //Parameter true to subscribe, false to unsubscribe. + function pubSubHubbubSubscribe($state, $secret = '') { + if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { + $callbackUrl = checkUrl(FreshRSS_Context::$system_conf->base_url . 'api/pshb.php?s=' . $secret); + if ($callbackUrl == '') { + return false; + } + + $ch = curl_init(); + curl_setopt_array($ch, array( + CURLOPT_URL => $this->hubUrl, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')', + CURLOPT_POSTFIELDS => 'hub.verify=sync' + . '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe') + . '&hub.topic=' . urlencode($this->selfUrl) + . '&hub.callback=' . urlencode($callbackUrl) + ) + ); + $response = curl_exec($ch); + $info = curl_getinfo($ch); + + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . + 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl . + ' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND); + return substr($info['http_code'], 0, 1) == '2'; + } + return false; + } + + // } diff --git a/constants.php b/constants.php index b20bf0710..5bb410e29 100644 --- a/constants.php +++ b/constants.php @@ -18,6 +18,7 @@ define('FRESHRSS_PATH', dirname(__FILE__)); define('UPDATE_FILENAME', DATA_PATH . '/update.php'); define('USERS_PATH', DATA_PATH . '/users'); define('CACHE_PATH', DATA_PATH . '/cache'); + define('PSHB_PATH', DATA_PATH . '/PubSubHubbub'); define('LIB_PATH', FRESHRSS_PATH . '/lib'); define('APP_PATH', FRESHRSS_PATH . '/app'); diff --git a/data/PubSubHubbub/feeds/.gitignore b/data/PubSubHubbub/feeds/.gitignore new file mode 100644 index 000000000..150f68c80 --- /dev/null +++ b/data/PubSubHubbub/feeds/.gitignore @@ -0,0 +1 @@ +*/* diff --git a/data/PubSubHubbub/feeds/README.md b/data/PubSubHubbub/feeds/README.md new file mode 100644 index 000000000..15fa8e521 --- /dev/null +++ b/data/PubSubHubbub/feeds/README.md @@ -0,0 +1,12 @@ +List of canonical URLS of the various feeds users have subscribed to. +Several feeds can share the same canonical URL (rel="self"). +Several users can have subscribed to the same feed. + +* ./base64url(canonicalUrl)/ + * ./secret.txt + * ./base64url(feedUrl1)/ + * ./user1.txt + * ./user2.txt + * ./base64url(feedUrl2)/ + * ./user3.txt + * ./user4.txt diff --git a/data/PubSubHubbub/secrets/.gitignore b/data/PubSubHubbub/secrets/.gitignore new file mode 100644 index 000000000..2211df63d --- /dev/null +++ b/data/PubSubHubbub/secrets/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/data/PubSubHubbub/secrets/README.md b/data/PubSubHubbub/secrets/README.md new file mode 100644 index 000000000..ad8158839 --- /dev/null +++ b/data/PubSubHubbub/secrets/README.md @@ -0,0 +1,4 @@ +List of secrets given to PubSubHubbub hubs + +* ./sha1(random + salt).txt + * base64url(canonicalUrl) diff --git a/data/config.default.php b/data/config.default.php index 8be203d36..80d331df7 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -11,9 +11,11 @@ return array( # 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. + # Specify address of the FreshRSS instance, + # used when building absolute URLs, e.g. for PubSubHubbub. + # Examples: + # https://example.net/FreshRSS/p/ + # https://freshrss.example.net/ 'base_url' => '', # Natural language of the user interface, e.g. `en`, `fr`. diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 6342011c8..191a58f35 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -446,3 +446,12 @@ function array_push_unique(&$array, $value) { function array_remove(&$array, $value) { $array = array_diff($array, array($value)); } + +//RFC 4648 +function base64url_encode($data) { + return strtr(rtrim(base64_encode($data), '='), '+/', '-_'); +} +//RFC 4648 +function base64url_decode($data) { + return base64_decode(strtr($data, '-_', '+/')); +} diff --git a/p/api/pshb.php b/p/api/pshb.php new file mode 100644 index 000000000..bcb8341b1 --- /dev/null +++ b/p/api/pshb.php @@ -0,0 +1,116 @@ + $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true)); + +$secret = isset($_GET['s']) ? substr($_GET['s'], 0, 128) : ''; +if (!ctype_xdigit($secret)) { + header('HTTP/1.1 422 Unprocessable Entity'); + die('Invalid feed secret format!'); +} +chdir(PSHB_PATH); +$canonical64 = @file_get_contents('secrets/' . $secret . '.txt'); +if ($canonical64 === false) { + header('HTTP/1.1 404 Not Found'); + logMe('Feed secret not found!: ' . $secret); + die('Feed secret not found!'); +} +$canonical64 = trim($canonical64); +if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) { + header('HTTP/1.1 500 Internal Server Error'); + logMe('Invalid secret reference!: ' . $canonical64); + die('Invalid secret reference!'); +} +$secret2 = @file_get_contents('feeds/' . $canonical64 . '/secret.txt'); +if ($secret2 === false) { + header('HTTP/1.1 404 Not Found'); + //@unlink('secrets/' . $secret . '.txt'); + logMe('Feed reverse secret not found!: ' . $canonical64); + die('Feed reverse secret not found!'); +} +if ($secret !== $secret2) { + header('HTTP/1.1 500 Internal Server Error'); + logMe('Invalid secret cross-check!: ' . $secret); + die('Invalid secret cross-check!'); +} +chdir('feeds/' . $canonical64); +$users = glob('*/*.txt', GLOB_NOSORT); +if (empty($users)) { + header('HTTP/1.1 410 Gone'); + logMe('Nobody is subscribed to this feed anymore!: ' . $canonical64); + die('Nobody is subscribed to this feed anymore!'); +} + +if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') { + //TODO: hub_lease_seconds + exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : ''); +} + +Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php'); +$system_conf = Minz_Configuration::get('system'); +$system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!) +Minz_Translate::init('en'); +Minz_Request::_param('ajax', true); +$feedController = new FreshRSS_feed_Controller(); + +$simplePie = customSimplePie(); +$simplePie->set_raw_data($ORIGINAL_INPUT); +$simplePie->init(); +unset($ORIGINAL_INPUT); + +$links = $simplePie->get_links('self'); +$self = isset($links[0]) ? $links[0] : null; + +if ($self !== base64url_decode($canonical64)) { + header('HTTP/1.1 422 Unprocessable Entity'); + logMe('Self URL does not match registered canonical URL!: ' . $self); + die('Self URL does not match registered canonical URL!'); +} +Minz_Request::_param('url', $self); + +$nb = 0; +foreach ($users as $userLine) { + $userLine = strtr($userLine, '\\', '/'); + $userInfos = explode('/', $userLine); + $feedUrl = isset($userInfos[0]) ? base64url_decode($userInfos[0]) : ''; + $username = isset($userInfos[1]) ? basename($userInfos[1], '.txt') : ''; + if (!file_exists(USERS_PATH . '/' . $username . '/config.php')) { + break; + } + + try { + Minz_Session::_param('currentUser', $username); + Minz_Configuration::register('user', + join_path(USERS_PATH, $username, 'config.php'), + join_path(USERS_PATH, '_', 'config.default.php')); + FreshRSS_Context::init(); + if ($feedController->actualizeAction($simplePie) > 0) { + $nb++; + } + } catch (Exception $e) { + logMe($e->getMessage()); + } +} + +$simplePie->__destruct(); +unset($simplePie); + +if ($nb === 0) { + header('HTTP/1.1 410 Gone'); + logMe('Nobody is subscribed to this feed anymore after all!: ' . $self); + die('Nobody is subscribed to this feed anymore after all!'); +} + +logMe($self . ' done: ' . $nb); +exit('Done: ' . $nb . "\n"); -- cgit v1.2.3 From c472569b3861541c322c850c4ff8ca3471572f40 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 15 May 2015 15:34:51 +0200 Subject: First alpha of PubSubHubbub https://github.com/FreshRSS/FreshRSS/issues/312 Using a white list limited to http://push-pub.appspot.com/feed for alpha testing. --- app/Controllers/feedController.php | 31 ++++++++++++++------ app/Models/Feed.php | 53 +++++++++++++++++++++++++--------- data/PubSubHubbub/feeds/README.md | 11 ++------ data/PubSubHubbub/keys/.gitignore | 1 + data/PubSubHubbub/keys/README.md | 4 +++ data/PubSubHubbub/secrets/.gitignore | 1 - data/PubSubHubbub/secrets/README.md | 4 --- p/api/pshb.php | 55 ++++++++++++++++++++---------------- 8 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 data/PubSubHubbub/keys/.gitignore create mode 100644 data/PubSubHubbub/keys/README.md delete mode 100644 data/PubSubHubbub/secrets/.gitignore delete mode 100644 data/PubSubHubbub/secrets/README.md diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 9117da639..0fb4bdf03 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -304,6 +304,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { continue; } + $url = $feed->url(); //For detection of HTTP 301 try { if ($simplePie) { $feed->loadEntries($simplePie); //Used by PubSubHubbub @@ -317,7 +318,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! @@ -404,19 +404,34 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->commit(); } - if ($feed->url() !== $url) { - // HTTP 301 Moved Permanently + if ($feed->hubUrl() && $feed->selfUrl()) { //selfUrl has priority for PubSubHubbub + if ($feed->selfUrl() !== $url) { //https://code.google.com/p/pubsubhubbub/wiki/MovingFeedsOrChangingHubs + $selfUrl = checkUrl($feed->selfUrl()); + if ($selfUrl) { + Minz_Log::debug('PubSubHubbub unsubscribe ' . $feed->url()); + if (!$feed->pubSubHubbubSubscribe(false)) { //Unsubscribe + Minz_Log::warning('Error while PubSubHubbub unsubscribing from ' . $feed->url()); + } + $feed->_url($selfUrl, false); + Minz_Log::notice('Feed ' . $url . ' canonical address moved to ' . $feed->url()); + $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); + } + } + } + elseif ($feed->url() !== $url) { // HTTP 301 Moved Permanently Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url()); $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } if ($simplePie === null) { $feed->faviconPrepare(); - if ($feed->url() === 'http://push-pub.appspot.com/feed') { - $secret = $feed->pubSubHubbubPrepare(); - if ($secret != '') { - Minz_Log::debug('PubSubHubbub subscribe ' . $feed->url()); - $feed->pubSubHubbubSubscribe(true, $secret); + if (in_array($feed->url(), array('http://push-pub.appspot.com/feed'))) { //TODO: Remove white-list after testing + Minz_Log::debug('PubSubHubbub match ' . $feed->url()); + if ($feed->pubSubHubbubPrepare()) { + Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url()); + if (!$feed->pubSubHubbubSubscribe(true)) { //Subscribe + Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url()); + } } } } diff --git a/app/Models/Feed.php b/app/Models/Feed.php index dcf083ea8..a17cf415d 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -51,6 +51,12 @@ class FreshRSS_Feed extends Minz_Model { public function url() { return $this->url; } + public function selfUrl() { + return $this->selfUrl; + } + public function hubUrl() { + return $this->hubUrl; + } public function category() { return $this->category; } @@ -344,38 +350,59 @@ class FreshRSS_Feed extends Minz_Model { // function pubSubHubbubPrepare() { - $secret = ''; + $key = ''; if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { $path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl); - if (!file_exists($path . '/hub.txt')) { + if ($hubFile = @file_get_contents($path . '/!hub.json')) { + $hubJson = json_decode($hubFile, true); + if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { + Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url); + return false; + } + if (empty($hubJson['lease_end']) || $hubJson['lease_end'] <= time()) { + Minz_Log::warning('PubSubHubbub lease expired: ' . $this->url); + $key = $hubJson['key']; //To renew our lease + } + } else { @mkdir($path, 0777, true); - file_put_contents($path . '/hub.txt', $this->hubUrl); - $secret = sha1(FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true)); - file_put_contents($path . '/secret.txt', $secret); - @mkdir(PSHB_PATH . '/secrets/'); - file_put_contents(PSHB_PATH . '/secrets/' . $secret . '.txt', base64url_encode($this->selfUrl)); + $key = sha1(FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true)); + $hubJson = array( + 'hub' => $this->hubUrl, + 'key' => $key, + ); + file_put_contents($path . '/!hub.json', json_encode($hubJson)); + @mkdir(PSHB_PATH . '/keys/'); + file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', base64url_encode($this->selfUrl)); Minz_Log::notice('PubSubHubbub prepared for ' . $this->url); file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . 'PubSubHubbub prepared for ' . $this->url . "\n", FILE_APPEND); } - $path .= '/' . base64url_encode($this->url); $currentUser = Minz_Session::param('currentUser'); if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) { - @mkdir($path, 0777, true); touch($path . '/' . $currentUser . '.txt'); } } - return $secret; + return $key; } //Parameter true to subscribe, false to unsubscribe. - function pubSubHubbubSubscribe($state, $secret = '') { + function pubSubHubbubSubscribe($state) { if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { - $callbackUrl = checkUrl(FreshRSS_Context::$system_conf->base_url . 'api/pshb.php?s=' . $secret); + $hubFile = @file_get_contents(PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json'); + if ($hubFile === false) { + Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url); + return false; + } + $hubJson = json_decode($hubFile, true); + if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { + Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url); + return false; + } + $callbackUrl = checkUrl(FreshRSS_Context::$system_conf->base_url . 'api/pshb.php?k=' . $hubJson['key']); if ($callbackUrl == '') { + Minz_Log::warning('Invalid callback for PubSubHubbub: ' . $this->url); return false; } - $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $this->hubUrl, diff --git a/data/PubSubHubbub/feeds/README.md b/data/PubSubHubbub/feeds/README.md index 15fa8e521..a01a3197f 100644 --- a/data/PubSubHubbub/feeds/README.md +++ b/data/PubSubHubbub/feeds/README.md @@ -1,12 +1,7 @@ List of canonical URLS of the various feeds users have subscribed to. -Several feeds can share the same canonical URL (rel="self"). Several users can have subscribed to the same feed. * ./base64url(canonicalUrl)/ - * ./secret.txt - * ./base64url(feedUrl1)/ - * ./user1.txt - * ./user2.txt - * ./base64url(feedUrl2)/ - * ./user3.txt - * ./user4.txt + * ./!hub.json + * ./user1.txt + * ./user2.txt diff --git a/data/PubSubHubbub/keys/.gitignore b/data/PubSubHubbub/keys/.gitignore new file mode 100644 index 000000000..2211df63d --- /dev/null +++ b/data/PubSubHubbub/keys/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/data/PubSubHubbub/keys/README.md b/data/PubSubHubbub/keys/README.md new file mode 100644 index 000000000..bb1e57cd4 --- /dev/null +++ b/data/PubSubHubbub/keys/README.md @@ -0,0 +1,4 @@ +List of keys given to PubSubHubbub hubs + +* ./sha1(random + salt).txt + * base64url(canonicalUrl) diff --git a/data/PubSubHubbub/secrets/.gitignore b/data/PubSubHubbub/secrets/.gitignore deleted file mode 100644 index 2211df63d..000000000 --- a/data/PubSubHubbub/secrets/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.txt diff --git a/data/PubSubHubbub/secrets/README.md b/data/PubSubHubbub/secrets/README.md deleted file mode 100644 index ad8158839..000000000 --- a/data/PubSubHubbub/secrets/README.md +++ /dev/null @@ -1,4 +0,0 @@ -List of secrets given to PubSubHubbub hubs - -* ./sha1(random + salt).txt - * base64url(canonicalUrl) diff --git a/p/api/pshb.php b/p/api/pshb.php index bcb8341b1..90d4c52bb 100644 --- a/p/api/pshb.php +++ b/p/api/pshb.php @@ -12,40 +12,41 @@ function logMe($text) { $ORIGINAL_INPUT = file_get_contents('php://input', false, null, -1, MAX_PAYLOAD); -logMe(print_r(array('_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true)); +logMe(print_r(array('_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true)); -$secret = isset($_GET['s']) ? substr($_GET['s'], 0, 128) : ''; -if (!ctype_xdigit($secret)) { +$key = isset($_GET['k']) ? substr($_GET['k'], 0, 128) : ''; +if (!ctype_xdigit($key)) { header('HTTP/1.1 422 Unprocessable Entity'); - die('Invalid feed secret format!'); + die('Invalid feed key format!'); } chdir(PSHB_PATH); -$canonical64 = @file_get_contents('secrets/' . $secret . '.txt'); +$canonical64 = @file_get_contents('keys/' . $key . '.txt'); if ($canonical64 === false) { header('HTTP/1.1 404 Not Found'); - logMe('Feed secret not found!: ' . $secret); - die('Feed secret not found!'); + logMe('Feed key not found!: ' . $key); + die('Feed key not found!'); } $canonical64 = trim($canonical64); if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) { header('HTTP/1.1 500 Internal Server Error'); - logMe('Invalid secret reference!: ' . $canonical64); - die('Invalid secret reference!'); + logMe('Invalid key reference!: ' . $canonical64); + die('Invalid key reference!'); } -$secret2 = @file_get_contents('feeds/' . $canonical64 . '/secret.txt'); -if ($secret2 === false) { +$hubFile = @file_get_contents('feeds/' . $canonical64 . '/!hub.json'); +if ($hubFile === false) { header('HTTP/1.1 404 Not Found'); - //@unlink('secrets/' . $secret . '.txt'); - logMe('Feed reverse secret not found!: ' . $canonical64); - die('Feed reverse secret not found!'); + //@unlink('keys/' . $key . '.txt'); + logMe('Feed info not found!: ' . $canonical64); + die('Feed info not found!'); } -if ($secret !== $secret2) { +$hubJson = json_decode($hubFile, true); +if (!$hubJson || empty($hubJson['key']) || $hubJson['key'] !== $key) { header('HTTP/1.1 500 Internal Server Error'); - logMe('Invalid secret cross-check!: ' . $secret); - die('Invalid secret cross-check!'); + logMe('Invalid key cross-check!: ' . $key); + die('Invalid key cross-check!'); } chdir('feeds/' . $canonical64); -$users = glob('*/*.txt', GLOB_NOSORT); +$users = glob('*.txt', GLOB_NOSORT); if (empty($users)) { header('HTTP/1.1 410 Gone'); logMe('Nobody is subscribed to this feed anymore!: ' . $canonical64); @@ -53,10 +54,19 @@ if (empty($users)) { } if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') { - //TODO: hub_lease_seconds + $leaseSeconds = empty($_REQUEST['hub_lease_seconds']) ? 0 : intval($_REQUEST['hub_lease_seconds']); + if ($leaseSeconds > 60) { + $hubJson['lease_end'] = time() + $leaseSeconds; + file_put_contents('./!hub.json', json_encode($hubJson)); + } exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : ''); } +if ($ORIGINAL_INPUT == '') { + header('HTTP/1.1 422 Unprocessable Entity'); + die('Missing XML payload!'); +} + Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php'); $system_conf = Minz_Configuration::get('system'); $system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!) @@ -80,11 +90,8 @@ if ($self !== base64url_decode($canonical64)) { Minz_Request::_param('url', $self); $nb = 0; -foreach ($users as $userLine) { - $userLine = strtr($userLine, '\\', '/'); - $userInfos = explode('/', $userLine); - $feedUrl = isset($userInfos[0]) ? base64url_decode($userInfos[0]) : ''; - $username = isset($userInfos[1]) ? basename($userInfos[1], '.txt') : ''; +foreach ($users as $userFilename) { + $username = basename($userFilename, '.txt'); if (!file_exists(USERS_PATH . '/' . $username . '/config.php')) { break; } -- cgit v1.2.3 From 5adaf177210bcd7af0df30d8bebea9b3de67b443 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 15 May 2015 15:38:54 +0200 Subject: 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! -- cgit v1.2.3 From 18831a89efadf8a05fcc3285fa6af0051e41df2b Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 15 May 2015 15:46:51 +0200 Subject: PubSubHubbub active only when refreshing, not adding a feed https://github.com/FreshRSS/FreshRSS/issues/312 --- 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 0fb4bdf03..ab73879d0 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -168,7 +168,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Ok, feed has been added in database. Now we have to refresh entries. $feed->_id($id); $feed->faviconPrepare(); - $feed->pubSubHubbubPrepare(); + //$feed->pubSubHubbubPrepare(); //TODO: prepare PubSubHubbub already when adding the feed $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; -- cgit v1.2.3 From 0163564b9e02bc399c26d3083048f38d3374cbd7 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 15 May 2015 17:58:56 +0200 Subject: Change some error messages --- app/Models/Feed.php | 2 +- p/api/pshb.php | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/Models/Feed.php b/app/Models/Feed.php index a17cf415d..d2b552265 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -373,7 +373,7 @@ class FreshRSS_Feed extends Minz_Model { file_put_contents($path . '/!hub.json', json_encode($hubJson)); @mkdir(PSHB_PATH . '/keys/'); file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', base64url_encode($this->selfUrl)); - Minz_Log::notice('PubSubHubbub prepared for ' . $this->url); + Minz_Log::debug('PubSubHubbub prepared for ' . $this->url); file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . 'PubSubHubbub prepared for ' . $this->url . "\n", FILE_APPEND); } diff --git a/p/api/pshb.php b/p/api/pshb.php index 90d4c52bb..6280c04ac 100644 --- a/p/api/pshb.php +++ b/p/api/pshb.php @@ -12,7 +12,7 @@ function logMe($text) { $ORIGINAL_INPUT = file_get_contents('php://input', false, null, -1, MAX_PAYLOAD); -logMe(print_r(array('_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true)); +//logMe(print_r(array('_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true)); $key = isset($_GET['k']) ? substr($_GET['k'], 0, 128) : ''; if (!ctype_xdigit($key)) { @@ -23,33 +23,33 @@ chdir(PSHB_PATH); $canonical64 = @file_get_contents('keys/' . $key . '.txt'); if ($canonical64 === false) { header('HTTP/1.1 404 Not Found'); - logMe('Feed key not found!: ' . $key); + logMe('Error: Feed key not found!: ' . $key); die('Feed key not found!'); } $canonical64 = trim($canonical64); if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) { header('HTTP/1.1 500 Internal Server Error'); - logMe('Invalid key reference!: ' . $canonical64); + logMe('Error: Invalid key reference!: ' . $canonical64); die('Invalid key reference!'); } $hubFile = @file_get_contents('feeds/' . $canonical64 . '/!hub.json'); if ($hubFile === false) { header('HTTP/1.1 404 Not Found'); //@unlink('keys/' . $key . '.txt'); - logMe('Feed info not found!: ' . $canonical64); + logMe('Error: Feed info not found!: ' . $canonical64); die('Feed info not found!'); } $hubJson = json_decode($hubFile, true); if (!$hubJson || empty($hubJson['key']) || $hubJson['key'] !== $key) { header('HTTP/1.1 500 Internal Server Error'); - logMe('Invalid key cross-check!: ' . $key); + logMe('Error: Invalid key cross-check!: ' . $key); die('Invalid key cross-check!'); } chdir('feeds/' . $canonical64); $users = glob('*.txt', GLOB_NOSORT); if (empty($users)) { header('HTTP/1.1 410 Gone'); - logMe('Nobody is subscribed to this feed anymore!: ' . $canonical64); + logMe('Error: Nobody is subscribed to this feed anymore!: ' . $canonical64); die('Nobody is subscribed to this feed anymore!'); } @@ -83,9 +83,10 @@ $links = $simplePie->get_links('self'); $self = isset($links[0]) ? $links[0] : null; if ($self !== base64url_decode($canonical64)) { - header('HTTP/1.1 422 Unprocessable Entity'); - logMe('Self URL does not match registered canonical URL!: ' . $self); - die('Self URL does not match registered canonical URL!'); + //header('HTTP/1.1 422 Unprocessable Entity'); + logMe('Warning: Self URL ' . $self . ' does not match registered canonical URL!: ' . base64url_decode($canonical64)); + //die('Self URL does not match registered canonical URL!'); + $self = base64url_decode($canonical64); } Minz_Request::_param('url', $self); @@ -106,7 +107,7 @@ foreach ($users as $userFilename) { $nb++; } } catch (Exception $e) { - logMe($e->getMessage()); + logMe('Error: ' . $e->getMessage()); } } @@ -115,7 +116,7 @@ unset($simplePie); if ($nb === 0) { header('HTTP/1.1 410 Gone'); - logMe('Nobody is subscribed to this feed anymore after all!: ' . $self); + logMe('Error: Nobody is subscribed to this feed anymore after all!: ' . $self); die('Nobody is subscribed to this feed anymore after all!'); } -- cgit v1.2.3 From c6e2e1aa515d1941056f5da575b050d4da0b3d92 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Fri, 15 May 2015 20:09:58 +0200 Subject: 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( -- cgit v1.2.3 From b14b1a11dc2d74b5fbb220af83afb771a022d2a2 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Fri, 15 May 2015 20:18:24 +0200 Subject: 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 64c2c0945..04ebfab7e 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( @@ -135,14 +135,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', @@ -150,13 +150,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.', ), -- cgit v1.2.3 From 3adab4b70fab858048bd68ed72e71676c4d5badf Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 13:05:43 +0200 Subject: More PubSubHubbub https://github.com/FreshRSS/FreshRSS/issues/312 Show whether PubSubHubbub is enabled in the Web interface of feed configuration. When PubSubHubbub is used, do not pull refresh so often (hard-coded to max once per 24h for now). Improved logic for lease renewal, and some detection of lease problems. Updated read-me and changelog. --- CHANGELOG | 9 ++++++ README.fr.md | 22 +++++++------- README.md | 34 ++++++++++----------- app/Controllers/feedController.php | 36 ++++++++++++++-------- app/Models/Feed.php | 59 +++++++++++++++++++++++++++++++------ app/i18n/cz/conf.php | 1 + app/i18n/cz/sub.php | 1 + app/i18n/de/sub.php | 1 + app/i18n/en/sub.php | 1 + app/i18n/fr/sub.php | 1 + app/views/helpers/feed/update.phtml | 8 +++++ data/users/_/config.default.php | 2 +- p/api/pshb.php | 8 +++-- 13 files changed, 128 insertions(+), 55 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d1b49d339..f3559ccc4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,15 @@ ## 2015-xx-xx FreshRSS 1.1.1 (beta) +* Features + * Support for PubSubHubbub for instant notifications from compatible Web sites. + * 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..1110eb8e5 100644 --- a/README.fr.md +++ b/README.fr.md @@ -6,6 +6,7 @@ FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed] Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme. +Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des notifications instantanées depuis les sites compatibles. * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ @@ -14,28 +15,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 +61,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 +73,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..4430560fe 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ * [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. +It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant notifications from compatible Web sites. * Official website: http://freshrss.org * Demo: http://demo.freshrss.org/ @@ -14,28 +15,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 +43,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 +57,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 diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index ab73879d0..dfdf0dc16 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -268,7 +268,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { * If id and url are not specified, all the feeds are actualized. But if force is * false, process stops at 10 feeds to avoid time execution problem. */ - public function actualizeAction($simplePie = null) { + public function actualizeAction($simplePiePush = null) { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -295,10 +295,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Calculate date of oldest entries we accept in DB. $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration. $updated_feeds = 0; $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { + $pubSubHubbubEnabled = $feed->pubSubHubbubEnabled(); + if ((!$simplePiePush) && (!$id) && (!$force) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) { + continue; //When PubSubHubbub is used, do not pull refresh so often + } + if (!$feed->lock()) { Minz_Log::notice('Feed already being actualized: ' . $feed->url()); continue; @@ -306,8 +312,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $url = $feed->url(); //For detection of HTTP 301 try { - if ($simplePie) { - $feed->loadEntries($simplePie); //Used by PubSubHubbub + if ($simplePiePush) { + $feed->loadEntries($simplePiePush); //Used by PubSubHubbub } else { $feed->load(false); } @@ -374,6 +380,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController { continue; } + if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull! + $text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . ' GUID ' . $entry->guid(); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); + Minz_Log::warning($text); + $pubSubHubbubEnabled = false; + $feed->pubSubHubbubEnabled(false); //To force the renewal of our lease + } + if (!$entryDAO->hasTransaction()) { $entryDAO->beginTransaction(); } @@ -423,15 +437,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } - if ($simplePie === null) { - $feed->faviconPrepare(); - if (in_array($feed->url(), array('http://push-pub.appspot.com/feed'))) { //TODO: Remove white-list after testing - Minz_Log::debug('PubSubHubbub match ' . $feed->url()); - if ($feed->pubSubHubbubPrepare()) { - Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url()); - if (!$feed->pubSubHubbubSubscribe(true)) { //Subscribe - Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url()); - } + $feed->faviconPrepare(); + if (in_array($feed->url(), array('http://push-pub.appspot.com/feed'))) { //TODO: Remove white-list after testing + Minz_Log::debug('PubSubHubbub match ' . $feed->url()); + if ($feed->pubSubHubbubPrepare()) { + Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url()); + if (!$feed->pubSubHubbubSubscribe(true)) { //Subscribe + Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url()); } } } diff --git a/app/Models/Feed.php b/app/Models/Feed.php index d2b552265..7bc60dfc9 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -104,6 +104,16 @@ class FreshRSS_Feed extends Minz_Model { public function ttl() { return $this->ttl; } + // public function ttlExpire() { + // $ttl = $this->ttl; + // if ($ttl == -2) { //Default + // $ttl = FreshRSS_Context::$user_conf->ttl_default; + // } + // if ($ttl == -1) { //Never + // $ttl = 64000000; //~2 years. Good enough for PubSubHubbub logic + // } + // return $this->lastUpdate + $ttl; + // } public function nbEntries() { if ($this->nbEntries < 0) { $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -349,18 +359,42 @@ class FreshRSS_Feed extends Minz_Model { // + function pubSubHubbubEnabled($keep = true) { + $url = $this->selfUrl ? $this->selfUrl : $this->url; + $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json'; + if ($hubFile = @file_get_contents($hubFilename)) { + $hubJson = json_decode($hubFile, true); + if (!$keep) { + $hubJson['lease_end'] = time() - 60; + file_put_contents($hubFilename, json_encode($hubJson)); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" + . 'Force expire lease for ' . $url . "\n", FILE_APPEND); + } elseif ($hubJson && (empty($hubJson['lease_end']) || $hubJson['lease_end'] > time())) { + return true; + } + } + return false; + } + function pubSubHubbubPrepare() { $key = ''; if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { $path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl); - if ($hubFile = @file_get_contents($path . '/!hub.json')) { + $hubFilename = $path . '/!hub.json'; + if ($hubFile = @file_get_contents($hubFilename)) { $hubJson = json_decode($hubFile, true); if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { - Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url); + $text = 'Invalid JSON for PubSubHubbub: ' . $this->url; + Minz_Log::warning($text); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); return false; } - if (empty($hubJson['lease_end']) || $hubJson['lease_end'] <= time()) { - Minz_Log::warning('PubSubHubbub lease expired: ' . $this->url); + if (empty($hubJson['lease_end']) || ($hubJson['lease_end'] <= (time() + (3600 * 24)))) { //TODO: Make a better policy + $text = 'PubSubHubbub lease ends at ' + . date('c', empty($hubJson['lease_end']) ? time() : $hubJson['lease_end']) + . ' and needs renewal: ' . $this->url; + Minz_Log::warning($text); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); $key = $hubJson['key']; //To renew our lease } } else { @@ -370,12 +404,12 @@ class FreshRSS_Feed extends Minz_Model { 'hub' => $this->hubUrl, 'key' => $key, ); - file_put_contents($path . '/!hub.json', json_encode($hubJson)); + file_put_contents($hubFilename, json_encode($hubJson)); @mkdir(PSHB_PATH . '/keys/'); file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', base64url_encode($this->selfUrl)); - Minz_Log::debug('PubSubHubbub prepared for ' . $this->url); - file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . - 'PubSubHubbub prepared for ' . $this->url . "\n", FILE_APPEND); + $text = 'PubSubHubbub prepared for ' . $this->url; + Minz_Log::debug($text); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); } $currentUser = Minz_Session::param('currentUser'); if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) { @@ -388,7 +422,8 @@ class FreshRSS_Feed extends Minz_Model { //Parameter true to subscribe, false to unsubscribe. function pubSubHubbubSubscribe($state) { if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { - $hubFile = @file_get_contents(PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json'); + $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json'; + $hubFile = @file_get_contents($hubFilename); if ($hubFile === false) { Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url); return false; @@ -421,6 +456,12 @@ class FreshRSS_Feed extends Minz_Model { file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl . ' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND); + + if (!$state) { //unsubscribe + $hubJson['lease_end'] = time() - 60; + file_put_contents($hubFilename, json_encode($hubJson)); + } + return substr($info['http_code'], 0, 1) == '2'; } return false; diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php index 29fb1e4d4..9518df66d 100644 --- a/app/i18n/cz/conf.php +++ b/app/i18n/cz/conf.php @@ -84,6 +84,7 @@ return array( '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', + 'mark_updated_article_unread' => 'Označte aktualizované položky jako nepřečtené', '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é', diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index 78712506c..cea0541e3 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'URL kanálu', 'validator' => 'Zkontrolovat platnost kanálu', 'website' => 'URL webové stránky', + 'pubsubhubbub' => 'Okamžité oznámení s PubSubHubbub', ), 'import_export' => array( 'export' => 'Export', diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 0479b8f46..7433bd61c 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'Feed-URL', 'validator' => 'Überprüfen Sie die Gültigkeit des Feeds', 'website' => 'Webseiten-URL', + 'pubsubhubbub' => 'Sofortige Benachrichtigung mit PubSubHubbub', ), 'import_export' => array( 'export' => 'Exportieren', diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index 2b62e4775..d8b5ced04 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'Feed URL', 'validator' => 'Check the validity of the feed', 'website' => 'Website URL', + 'pubsubhubbub' => 'Instant notification with PubSubHubbub', ), 'import_export' => array( 'export' => 'Export', diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index a3f7c4d6d..0a1a03e41 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'URL du flux', 'validator' => 'Vérifier la valididé du flux', 'website' => 'URL du site', + 'pubsubhubbub' => 'Notification instantanée par PubSubHubbub', ), 'import_export' => array( 'export' => 'Exporter', diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index 0b08d036c..b2cf9f93c 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -126,6 +126,14 @@ ?> +
    + +
    + +
    +
    diff --git a/data/users/_/config.default.php b/data/users/_/config.default.php index bf74ca1de..8f8ff528c 100644 --- a/data/users/_/config.default.php +++ b/data/users/_/config.default.php @@ -25,7 +25,7 @@ return array ( # 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, + 'mark_updated_article_unread' => false, //TODO: -1 => ignore, 0 => update, 1 => update and mark as unread 'sort_order' => 'DESC', 'anon_access' => false, diff --git a/p/api/pshb.php b/p/api/pshb.php index 6280c04ac..2f7f48cd8 100644 --- a/p/api/pshb.php +++ b/p/api/pshb.php @@ -57,8 +57,10 @@ if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') { $leaseSeconds = empty($_REQUEST['hub_lease_seconds']) ? 0 : intval($_REQUEST['hub_lease_seconds']); if ($leaseSeconds > 60) { $hubJson['lease_end'] = time() + $leaseSeconds; - file_put_contents('./!hub.json', json_encode($hubJson)); + } else { + unset($hubJson['lease_end']); } + file_put_contents('./!hub.json', json_encode($hubJson)); exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : ''); } @@ -84,7 +86,7 @@ $self = isset($links[0]) ? $links[0] : null; if ($self !== base64url_decode($canonical64)) { //header('HTTP/1.1 422 Unprocessable Entity'); - logMe('Warning: Self URL ' . $self . ' does not match registered canonical URL!: ' . base64url_decode($canonical64)); + logMe('Warning: Self URL [' . $self . '] does not match registered canonical URL!: ' . base64url_decode($canonical64)); //die('Self URL does not match registered canonical URL!'); $self = base64url_decode($canonical64); } @@ -120,5 +122,5 @@ if ($nb === 0) { die('Nobody is subscribed to this feed anymore after all!'); } -logMe($self . ' done: ' . $nb); +logMe('PubSubHubbub ' . $self . ' done: ' . $nb); exit('Done: ' . $nb . "\n"); -- cgit v1.2.3 From 247bc7578be4bcc1bdb1fb998f1304f5d9d34637 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:52:08 +0200 Subject: 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( -- cgit v1.2.3 From 7bd97184c9fbac58359f1c457e209ebdc838c635 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:54:23 +0200 Subject: 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 f3479ed53..cce364d50 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.', -- cgit v1.2.3 From 853957a74f00b668bdfc759c246217e5a05178a2 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:55:31 +0200 Subject: 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', -- cgit v1.2.3 From ca58a265e6a702e42e25f5bf2393896b5517b0be Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:58:57 +0200 Subject: 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.', -- cgit v1.2.3 From 9fe8a7c62dcd815065983613991f000b17444f5c Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Fri, 15 May 2015 20:09:58 +0200 Subject: 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( -- cgit v1.2.3 From bd21838bd0edd0169c583d25a475f9eea7636333 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Fri, 15 May 2015 20:18:24 +0200 Subject: 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.', ), -- cgit v1.2.3 From 70f0fc8d614ae49aa21ef9f1ce1b3f06e294512f Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:52:08 +0200 Subject: 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( -- cgit v1.2.3 From 7c6ce30f59295d06ad7fc7f6c3e935d058836877 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:54:23 +0200 Subject: 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.', -- cgit v1.2.3 From 450cc2d66f55817f85090ab742ebc351df9cbc43 Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:55:31 +0200 Subject: 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', -- cgit v1.2.3 From fe2f6c74b9b2f5ed0f4c3f57b4bf2828e38e1f6a Mon Sep 17 00:00:00 2001 From: thomasE1993 Date: Sat, 16 May 2015 18:58:57 +0200 Subject: 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.', -- cgit v1.2.3 From 044c4428062ea215c9fe2f46fb47078854048e61 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 19:36:42 +0200 Subject: 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', ), -- cgit v1.2.3 From d3af903301ce2b59d2720e94727945cf9ff31a40 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 21:47:26 +0200 Subject: i18n: German PubSubHubbub https://github.com/FreshRSS/FreshRSS/pull/831/files#r30463016 https://github.com/FreshRSS/FreshRSS/issues/312 --- app/i18n/de/sub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 7433bd61c..0f05a5635 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -37,7 +37,7 @@ return array( 'url' => 'Feed-URL', 'validator' => 'Überprüfen Sie die Gültigkeit des Feeds', 'website' => 'Webseiten-URL', - 'pubsubhubbub' => 'Sofortige Benachrichtigung mit PubSubHubbub', + 'pubsubhubbub' => 'Sofortbenachrichtigung mit PubSubHubbub', ), 'import_export' => array( 'export' => 'Exportieren', -- cgit v1.2.3 From 99dd1d37e021733f27f6e09129a7133e6db5e584 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 21:52:57 +0200 Subject: PubSubHubbub changelog Planned for version beta 1.1.2 https://github.com/FreshRSS/FreshRSS/issues/312 --- CHANGELOG | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f3559ccc4..ed1f655f9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,14 @@ # Changelog -## 2015-xx-xx FreshRSS 1.1.1 (beta) +## 2015-xx-xx FreshRSS 1.1.2 (beta) * Features * Support for PubSubHubbub for instant notifications from compatible Web sites. + + +## 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. -- cgit v1.2.3 From a19e908b73b3fe011bc40609d1edb7f7da6030c8 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 22:04:47 +0200 Subject: 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 -- cgit v1.2.3 From 005751d3fb43eefb57b52e8b8cbdf4da1a3578e8 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 16 May 2015 22:57:51 +0200 Subject: 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("