summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md53
-rw-r--r--README.fr.md80
-rw-r--r--README.md86
-rw-r--r--app/Controllers/categoryController.php5
-rwxr-xr-xapp/Controllers/configureController.php2
-rwxr-xr-xapp/Controllers/feedController.php318
-rw-r--r--app/Controllers/importExportController.php379
-rwxr-xr-xapp/Controllers/indexController.php44
-rw-r--r--app/Controllers/statsController.php36
-rw-r--r--app/Controllers/userController.php161
-rw-r--r--app/Exceptions/AlreadySubscribedException.php14
-rw-r--r--app/Exceptions/FeedNotAddedException.php14
-rw-r--r--app/Exceptions/ZipException.php14
-rw-r--r--app/Exceptions/ZipMissingException.php4
-rw-r--r--app/FreshRSS.php2
-rw-r--r--app/Models/CategoryDAO.php21
-rw-r--r--app/Models/ConfigurationSetter.php1
-rw-r--r--app/Models/Context.php20
-rw-r--r--app/Models/DatabaseDAOPGSQL.php43
-rw-r--r--app/Models/EntryDAO.php168
-rw-r--r--app/Models/EntryDAOPGSQL.php31
-rw-r--r--app/Models/EntryDAOSQLite.php20
-rw-r--r--app/Models/Factory.php42
-rw-r--r--app/Models/Feed.php41
-rw-r--r--app/Models/FeedDAO.php57
-rw-r--r--app/Models/FeedDAOSQLite.php4
-rw-r--r--app/Models/StatsDAO.php102
-rw-r--r--app/Models/StatsDAOPGSQL.php67
-rw-r--r--app/Models/StatsDAOSQLite.php59
-rw-r--r--app/Models/UserDAO.php54
-rw-r--r--app/SQL/install.sql.mysql.php7
-rw-r--r--app/SQL/install.sql.pgsql.php63
-rw-r--r--app/SQL/install.sql.sqlite.php36
-rwxr-xr-xapp/actualize_script.php3
-rw-r--r--app/i18n/cz/admin.php8
-rw-r--r--app/i18n/cz/feedback.php6
-rw-r--r--app/i18n/cz/install.php6
-rw-r--r--app/i18n/cz/sub.php4
-rw-r--r--app/i18n/de/admin.php8
-rw-r--r--app/i18n/de/feedback.php6
-rw-r--r--app/i18n/de/install.php8
-rw-r--r--app/i18n/de/sub.php2
-rw-r--r--app/i18n/en/admin.php20
-rw-r--r--app/i18n/en/feedback.php6
-rw-r--r--app/i18n/en/install.php26
-rw-r--r--app/i18n/en/sub.php4
-rw-r--r--app/i18n/fr/admin.php28
-rw-r--r--app/i18n/fr/feedback.php6
-rw-r--r--app/i18n/fr/install.php26
-rw-r--r--app/i18n/fr/sub.php4
-rw-r--r--app/i18n/it/admin.php8
-rw-r--r--app/i18n/it/feedback.php6
-rw-r--r--app/i18n/it/install.php12
-rw-r--r--app/i18n/it/sub.php4
-rw-r--r--app/i18n/nl/admin.php8
-rw-r--r--app/i18n/nl/feedback.php6
-rw-r--r--app/i18n/nl/install.php10
-rw-r--r--app/i18n/nl/sub.php4
-rw-r--r--app/i18n/ru/admin.php8
-rw-r--r--app/i18n/ru/feedback.php6
-rw-r--r--app/i18n/ru/install.php10
-rw-r--r--app/i18n/ru/sub.php4
-rw-r--r--app/i18n/tr/admin.php8
-rw-r--r--app/i18n/tr/feedback.php6
-rw-r--r--app/i18n/tr/install.php10
-rw-r--r--app/i18n/tr/sub.php4
-rw-r--r--app/install.php196
-rw-r--r--app/layout/aside_feed.phtml4
-rw-r--r--app/layout/aside_subscription.phtml2
-rw-r--r--app/layout/layout.phtml3
-rw-r--r--app/layout/nav_menu.phtml7
-rw-r--r--app/views/auth/index.phtml2
-rw-r--r--app/views/configure/sharing.phtml4
-rw-r--r--app/views/feed/add.phtml6
-rw-r--r--app/views/helpers/feed/update.phtml6
-rw-r--r--app/views/helpers/index/normal/entry_bottom.phtml4
-rw-r--r--app/views/helpers/index/normal/entry_header.phtml4
-rwxr-xr-xapp/views/helpers/pagination.phtml3
-rw-r--r--app/views/importExport/index.phtml2
-rw-r--r--app/views/index/global.phtml5
-rw-r--r--app/views/index/normal.phtml2
-rw-r--r--app/views/stats/index.phtml12
-rw-r--r--app/views/stats/repartition.phtml8
-rw-r--r--app/views/user/manage.phtml4
-rw-r--r--cli/.htaccess3
-rw-r--r--cli/README.md58
-rw-r--r--cli/_cli.php49
-rwxr-xr-xcli/actualize-user.php23
-rwxr-xr-xcli/create-user.php48
-rwxr-xr-xcli/delete-user.php32
-rwxr-xr-xcli/do-install.php101
-rwxr-xr-xcli/export-opml-for-user.php24
-rwxr-xr-xcli/export-zip-for-user.php30
-rwxr-xr-xcli/import-for-user.php35
-rw-r--r--cli/index.html (renamed from data/persona/index.html)0
-rwxr-xr-xcli/list-users.php14
-rw-r--r--constants.php2
-rw-r--r--data/.gitignore10
-rw-r--r--data/PubSubHubbub/feeds/.gitignore4
-rw-r--r--data/config.default.php3
-rw-r--r--data/persona/.gitignore1
-rw-r--r--data/users/.gitignore9
-rw-r--r--data/users/_/config.default.php8
-rw-r--r--lib/Favicon/DataAccess.php2
-rw-r--r--lib/Minz/Log.php6
-rw-r--r--lib/Minz/ModelPdo.php101
-rw-r--r--lib/Minz/Url.php2
-rw-r--r--lib/SimplePie/SimplePie/Content/Type/Sniffer.php15
-rw-r--r--lib/favicons.php40
-rw-r--r--lib/lib_install.php115
-rw-r--r--lib/lib_rss.php21
-rw-r--r--p/api/greader.php244
-rw-r--r--p/api/pshb.php8
-rw-r--r--p/f.php58
-rw-r--r--p/scripts/category.js1
-rw-r--r--p/scripts/global_view.js1
-rw-r--r--p/scripts/install.js6
-rw-r--r--p/scripts/jquery.min.js8
-rw-r--r--p/scripts/main.js82
-rw-r--r--p/themes/Pafat/pafat.css3
-rw-r--r--p/themes/base-theme/template.css10
121 files changed, 2546 insertions, 1238 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8a06eac0..461949128 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,56 @@
# Changelog
+## 2016-10-30 FreshRSS 1.6.0
+
+* CLI
+ * New Command-Line Interface (CLI) [#1095](https://github.com/FreshRSS/FreshRSS/issues/1095)
+ * Install, add/delete users, actualize, import/export. See [CLI documentation](./cli/README.md).
+* API
+ * Support for editing feeds and categories from client applications [#1254](https://github.com/FreshRSS/FreshRSS/issues/1254)
+* Compatibility:
+ * Support for PostgreSQL [#416](https://github.com/FreshRSS/FreshRSS/issues/416)
+ * New client supporting FreshRSS on Linux: FeedReader 2.0+ [#1252](https://github.com/FreshRSS/FreshRSS/issues/1252)
+* Features
+ * Rework the “mark as read during scroll” option, enabled by default for new users [#1258](https://github.com/FreshRSS/FreshRSS/issues/1258), [#1309](https://github.com/FreshRSS/FreshRSS/pull/1309)
+ * Including a *keep unread* function [#1327](https://github.com/FreshRSS/FreshRSS/pull/1327)
+ * In a multi-user context, take better advantage of other users’ refreshes [#1280](https://github.com/FreshRSS/FreshRSS/pull/1280)
+ * Better control of number of entries per page or RSS feed [#1249](https://github.com/FreshRSS/FreshRSS/issues/1249)
+ * Since X hours: `https://freshrss.example/i/?a=rss&hours=3`
+ * Explicit number: `https://freshrss.example/i/?a=rss&nb=10`
+ * Limited by `min_posts_per_rss` and `max_posts_per_rss` in user config
+ * Support custom ports `localhost:3306` for database servers [#1241](https://github.com/FreshRSS/FreshRSS/issues/1241)
+ * Add date to exported files [#1240](https://github.com/FreshRSS/FreshRSS/issues/1240)
+ * Auto-refresh favicons once or twice a month [#1181](https://github.com/FreshRSS/FreshRSS/issues/1181), [#1298](https://github.com/FreshRSS/FreshRSS/issues/1298)
+ * Cron updates will also refresh favicons every 2 weeks [#1306](https://github.com/FreshRSS/FreshRSS/pull/1306)
+* Bug fixing
+ * Correction of bugs related to CSRF tokens introduced in version 1.5.0 [#1253](https://github.com/FreshRSS/FreshRSS/issues/1253), [44f22ab](https://github.com/FreshRSS/FreshRSS/pull/1261/commits/d9bf9b2c6f0b2cc9dec3b638841b7e3040dcf46f)
+ * Fix bug in Global view introduced in version 1.5.0 [#1269](https://github.com/FreshRSS/FreshRSS/pull/1269)
+ * Fix sharing bug [#1289](https://github.com/FreshRSS/FreshRSS/issues/1289)
+ * Fix bug in auto-loading more articles after marking an article as un-read [#1318](https://github.com/FreshRSS/FreshRSS/issues/1318)
+ * Fix bug during import of favourites [#1315](https://github.com/FreshRSS/FreshRSS/pull/1315), [#1312](https://github.com/FreshRSS/FreshRSS/issues/1312)
+ * Fix bug not respecting language option for new users [#1273](https://github.com/FreshRSS/FreshRSS/issues/1273)
+ * Bug in example of URL for FreshRSS RSS output with token [#1274](https://github.com/FreshRSS/FreshRSS/issues/1274)
+* Security
+ * Prevent `<a target="_blank">` attacks with `window.opener` [#1245](https://github.com/FreshRSS/FreshRSS/issues/1245)
+ * Updated gitignore rules to keep user directories during a `git clean -f -d` [#1307](https://github.com/FreshRSS/FreshRSS/pull/1307)
+* Extensions
+ * Allow extensions for default account in anonymous mode [#1288](https://github.com/FreshRSS/FreshRSS/pull/1288)
+ * Trigger a `freshrss:load-more` JavaScript event to help extensions [#1278](https://github.com/FreshRSS/FreshRSS/issues/1278)
+* SQL
+ * Slightly modified several SQL requests (MySQL, SQLite) to simplify support of PostgreSQL [#1195](https://github.com/FreshRSS/FreshRSS/pull/1195)
+ * Increase performances by removing a superfluous category request [#1316](https://github.com/FreshRSS/FreshRSS/pull/1316)
+* I18n
+ * Fix some messages during installation [#1339](https://github.com/FreshRSS/FreshRSS/pull/1339)
+* UI
+ * Fix CSS line-height bug with `<sup>` in dates (English, Russian, Turkish) [#1340](https://github.com/FreshRSS/FreshRSS/pull/1340)
+ * Disable *Mark all as read* before confirmation script is loaded [#1342](https://github.com/FreshRSS/FreshRSS/issues/1342)
+ * Download icon 💾 for podcasts [#1236](https://github.com/FreshRSS/FreshRSS/issues/1236)
+* SimplePie
+ * Fix auto-discovery of RSS feeds in Web pages served as `text/xml` [#1264](https://github.com/FreshRSS/FreshRSS/issues/1264)
+* Mics.
+ * Removed *resource-priorities* attributes (`defer`, `lazyload`), deprecated by W3C [#1222](https://github.com/FreshRSS/FreshRSS/pull/1222)
+
+
## 2016-08-29 FreshRSS 1.5.0
* Compatibility
@@ -343,7 +394,7 @@
* Possibility to combine search filters, e.g. `date:2014-05 intitle:FreshRSS intitle:Open great reader #Internet`
* Change nav menu with more buttons instead of dropdown menus and add some filters
* New system of import / export
- * Support OPML, Json (like Google Reader) and Zip archives
+ * Support OPML, Json (like Google Reader) and ZIP archives
* Can export and import articles (specific option for favorites)
* Refactor "Origine" theme
* Some improvements
diff --git a/README.fr.md b/README.fr.md
index 99de1f618..8efa983e3 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -7,6 +7,7 @@ Il se veut léger et facile à prendre en main tout en étant un outil puissant
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.
+Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de commande](./cli/README.md).
* Site officiel : http://freshrss.org
* Démo : http://demo.freshrss.org/
@@ -17,11 +18,9 @@ Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des not
# Téléchargement
Voir la [liste des versions](../../releases).
-## Note sur les branches
-**Ce logiciel est en développement permanent !** Veuillez vous assurer d'utiliser la branche qui vous correspond :
-
+## À propos des branches
* Utilisez [la branche master](https://github.com/FreshRSS/FreshRSS/tree/master/) si vous visez la stabilité.
-* 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 !
+* Pour ceux qui veulent bien aider à tester ou déveloper les dernières fonctionnalités, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras !
# Avertissements
Cette application a été développée pour s’adapter principalement à des besoins personnels, et aucune garantie n'est fournie.
@@ -33,9 +32,9 @@ Nous sommes une communauté amicale.
* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
* Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
* PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances)
- * Requis : [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl)
- * Recommandés : [JSON](http://php.net/json), [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), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [Zip](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
-* MySQL 5.5.3+ (recommandé) ou SQLite 3.7.4+
+ * Requis : [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), et [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql)
+ * Recommandés : [JSON](http://php.net/json), [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), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [ZIP](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
+* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL (experimental)
* Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
* Fonctionne aussi sur mobile
@@ -46,48 +45,59 @@ Nous sommes une communauté amicale.
2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/`
4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation
-5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à me contacter.
+ * ou utilisez [l’interface en ligne de commande](./cli/README.md)
+5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à [nous contacter](https://github.com/FreshRSS/FreshRSS/issues).
6. Des paramètres de configuration avancée peuvent être accédés depuis [config.php](./data/config.default.php).
## Installation automatisée
-[![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
## Exemple d’installation complète sur Linux Debian/Ubuntu
```sh
# Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
sudo apt-get install apache2
-sudo a2enmod headers expires rewrite ssl
-# (optionnel) Si vous voulez un serveur de base de données MySQL
-sudo apt-get install mysql-server mysql-client php5-mysql
-# Composants principaux (pour Ubuntu <= 15.10, Debian <= 8 Jessie)
+sudo a2enmod headers expires rewrite ssl #Modules Apache
+
+# Pour Ubuntu <= 15.10, Debian <= 8 Jessie
sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-# Composants principaux (pour Ubuntu >= 16.04, Debian >= 9 Stretch)
-sudo apt install php libapache2-mod-php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
-# Redémarrage du serveur Web
+sudo apt-get install libapache2-mod-php5 #Pour Apache
+sudo apt-get install mysql-server mysql-client php5-mysql #Base de données MySQL optionnelle
+sudo apt-get install postgresql php5-pgsql #Base de données PostgreSQL optionnelle
+
+# Pour Ubuntu >= 16.04, Debian >= 9 Stretch
+sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt install libapache2-mod-php #Pour Apache
+sudo apt install mysql-server mysql-client php-mysql #Base de données MySQL optionnelle
+sudo apt install postgresql php-pgsql #Base de données PostgreSQL optionnelle
+
+## Redémarrage du serveur Web
sudo service apache2 restart
# Pour FreshRSS lui-même (git est optionnel si vous déployez manuellement les fichiers d’installation)
cd /usr/share/
sudo apt-get install git
sudo git clone https://github.com/FreshRSS/FreshRSS.git
-# Mettre les droits d’accès pour le serveur Web
cd FreshRSS
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+
+# Si vous souhaitez utiliser la branche développement de FreshRSS
+sudo git checkout -b dev origin/dev
+
+# Mettre les droits d’accès pour le serveur Web
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
# Publier FreshRSS dans votre répertoire HTML public
sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
-# Naviguez vers http://example.net/FreshRSS pour terminer l’installation.
+# Naviguez vers http://example.net/FreshRSS pour terminer l’installation
# (Si vous le faite depuis localhost, vous pourrez avoir à ajuster le réglage de votre adresse publique)
+# ou utilisez l’interface en ligne de commande
# Mettre à jour FreshRSS vers une nouvelle version
cd /usr/share/FreshRSS
-sudo git reset --hard
sudo git pull
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
```
-# Contrôle d’accès
+## Contrôle d’accès
Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix :
* En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+)
* En utilisant un contrôle d’accès HTTP défini par votre serveur Web
@@ -101,18 +111,28 @@ 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 /votre-chemin/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+8 * * * * php /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
+### Exemple pour Debian / Ubuntu
+Créer `/etc/cron.d/FreshRSS` avec :
+
+```
+7,37 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+```
+
+
# Conseils
* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
* Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici.
* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
+
# Sauvegarde
* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php`
* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
+ * soit depuis l’interface Web, soit [en ligne de commande](./cli/README.md)
* Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
```bash
@@ -138,3 +158,13 @@ mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
## Si les fonctions natives ne sont pas disponibles
* [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
* [password_compat](https://github.com/ircmaxell/password_compat)
+
+
+# Clients compatibles
+Tout client supportant une API de type Google Reader. Sélection :
+
+* Android
+ * [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) avec [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Propriétaire)
+ * [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, F-Droid)
+* Linux
+ * [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)
diff --git a/README.md b/README.md
index 846652878..ecdd19376 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ 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.
+There is an API for (mobile) clients, and a [Command-Line Interface](./cli/README.md).
* Official website: http://freshrss.org
* Demo: http://demo.freshrss.org/
@@ -17,15 +18,13 @@ It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant
# Releases
See the [list of releases](../../releases).
-## Note on branches
-**This application is under continuous development!** Please use the branch that suits your needs:
-
+## About branches
* Use [the master branch](https://github.com/FreshRSS/FreshRSS/tree/master/) if you need a stable version.
-* 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!
+* For those willing to help testing or developing the latest features, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you!
# Disclaimer
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).
+Feature requests, bug reports, and other contributions are welcome. The best way is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
We are a friendly community.
# Requirements
@@ -33,9 +32,9 @@ We are a friendly community.
* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
* A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
* PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance)
- * Required extensions: [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl)
- * Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [Zip](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
-* MySQL 5.5.3+ (recommended) or SQLite 3.7.4+
+ * Required extensions: [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), and [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql)
+ * Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [ZIP](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
+* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL (experimental)
* A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
* Works on mobile
@@ -46,23 +45,32 @@ We are a friendly community.
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. Everything should be working :) If you encounter any problem, feel free to contact me.
+ * or use the [Command-Line Interface](./cli/README.md)
+5. Everything should be working :) If you encounter any problem, feel free [contact us](https://github.com/FreshRSS/FreshRSS/issues).
6. Advanced configuration settings can be seen in [config.php](./data/config.default.php).
## Automated install
-[![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
+* [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
## Example of full installation on Linux Debian/Ubuntu
```sh
# If you use an Apache Web server (otherwise you need another Web server)
sudo apt-get install apache2
-sudo a2enmod headers expires rewrite ssl
-# (Optional) If you want a MySQL database server
-sudo apt-get install mysql-server mysql-client php5-mysql
-# Main components (for Ubuntu <= 15.10, Debian <= 8 Jessie)
+sudo a2enmod headers expires rewrite ssl #Apache modules
+
+# For Ubuntu <= 15.10, Debian <= 8 Jessie
sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-# Main components (for Ubuntu >= 16.04, Debian >= 9 Stretch)
-sudo apt install php libapache2-mod-php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt-get install libapache2-mod-php5 #For Apache
+sudo apt-get install mysql-server mysql-client php5-mysql #Optional MySQL database
+sudo apt-get install postgresql php5-pgsql #Optional PostgreSQL database
+
+# For Ubuntu >= 16.04, Debian >= 9 Stretch
+sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt install libapache2-mod-php #For Apache
+sudo apt install mysql-server mysql-client php-mysql #Optional MySQL database
+sudo apt install postgresql php-pgsql #Optional PostgreSQL database
+
# Restart Web server
sudo service apache2 restart
@@ -70,49 +78,61 @@ sudo service apache2 restart
cd /usr/share/
sudo apt-get install git
sudo git clone https://github.com/FreshRSS/FreshRSS.git
-# Set the rights so that your Web browser can access the files
cd FreshRSS
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+
+# If you want to use the development version of FreshRSS
+sudo git checkout -b dev origin/dev
+
+# Set the rights so that your Web server can access the files
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
# Publish FreshRSS in your public HTML directory
sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
-# Navigate to http://example.net/FreshRSS to complete the installation.
+# Navigate to http://example.net/FreshRSS to complete the installation
# (If you do it from localhost, you may have to adjust the setting of your public address later)
+# or use the Command-Line Interface
# Update to a newer version of FreshRSS
cd /usr/share/FreshRSS
-sudo git reset --hard
sudo git pull
-sudo chown -R :www-data .
-sudo chmod -R g+w ./data/
+sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
```
-# Access control
+## Access control
It is needed for the multi-user mode to limit access to FreshRSS. You can:
* use form authentication (need JavaScript and PHP 5.3.7+, works with some PHP 5.3.3+)
* use HTTP authentication supported by your web server
* See [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html)
* In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file.
-# Automatic feed update
+## 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.
-For example, if you want to run the script every hour:
+It is a good idea to use the Web server user.
+For instance, if you want to run the script every hour:
```
-7 * * * * php /your-path/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+9 * * * * php /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
+### Example on Debian / Ubuntu
+Create `/etc/cron.d/FreshRSS` with:
+
+```
+6,36 * * * * www-data php -f /usr/share/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 accessible from the interface or manually in `./data/log/*.log` files.
+
# Backup
* You need to keep `./data/config.php`, and `./data/*_user.php` files
* You can export your feed list in OPML format from FreshRSS
+ * either from the Web interface, or from the [Command-Line Interface](./cli/README.md)
* To save articles, you can use [phpMyAdmin](http://www.phpmyadmin.net) or MySQL tools:
```bash
@@ -138,3 +158,13 @@ mysqldump -u user -p --databases freshrss > freshrss.sql
## If native functions are not available
* [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198)
* [password_compat](https://github.com/ircmaxell/password_compat)
+
+
+# Compatible clients
+Any client supporting a Google Reader-like API. Selection:
+
+* Android
+ * [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Closed source)
+ * [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, F-Droid)
+* Linux
+ * [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php
index e65c146de..922f92844 100644
--- a/app/Controllers/categoryController.php
+++ b/app/Controllers/categoryController.php
@@ -117,7 +117,6 @@ class FreshRSS_category_Controller extends Minz_ActionController {
public function deleteAction() {
$feedDAO = FreshRSS_Factory::createFeedDao();
$catDAO = new FreshRSS_CategoryDAO();
- $default_category = $catDAO->getDefault();
$url_redirect = array('c' => 'subscription', 'a' => 'index');
if (Minz_Request::isPost()) {
@@ -128,11 +127,11 @@ class FreshRSS_category_Controller extends Minz_ActionController {
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
}
- if ($id === $default_category->id()) {
+ if ($id === FreshRSS_CategoryDAO::defaultCategoryId) {
Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect);
}
- if ($feedDAO->changeCategory($id, $default_category->id()) === false) {
+ if ($feedDAO->changeCategory($id, FreshRSS_CategoryDAO::defaultCategoryId) === false) {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php
index 147a2fe06..e73f106a6 100755
--- a/app/Controllers/configureController.php
+++ b/app/Controllers/configureController.php
@@ -139,7 +139,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
*/
public function sharingAction() {
if (Minz_Request::isPost()) {
- $params = Minz_Request::fetchGET();
+ $params = Minz_Request::fetchPOST();
FreshRSS_Context::$user_conf->sharing = $params['share'];
FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php
index ffda1450d..c4115584a 100755
--- a/app/Controllers/feedController.php
+++ b/app/Controllers/feedController.php
@@ -26,6 +26,62 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
}
+ public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') {
+ @set_time_limit(300);
+
+ $catDAO = new FreshRSS_CategoryDAO();
+
+ $cat = null;
+ if ($cat_id > 0) {
+ $cat = $catDAO->searchById($cat_id);
+ }
+ if ($cat == null && $new_cat_name != '') {
+ $cat = $catDAO->addCategory(array('name' => $new_cat_name));
+ }
+ if ($cat == null) {
+ $catDAO->checkDefault();
+ }
+ $cat_id = $cat == null ? FreshRSS_CategoryDAO::defaultCategoryId : $cat->id();
+
+ $feed = new FreshRSS_Feed($url); //Throws FreshRSS_BadUrl_Exception
+ $feed->_httpAuth($http_auth);
+ $feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
+ $feed->_category($cat_id);
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ if ($feedDAO->searchByUrl($feed->url())) {
+ throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name());
+ }
+
+ // Call the extension hook
+ $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
+ if ($feed === null) {
+ throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+ }
+
+ $values = array(
+ 'url' => $feed->url(),
+ 'category' => $feed->category(),
+ 'name' => $title != '' ? $title : $feed->name(),
+ 'website' => $feed->website(),
+ 'description' => $feed->description(),
+ 'lastUpdate' => time(),
+ 'httpAuth' => $feed->httpAuth(),
+ );
+
+ $id = $feedDAO->addFeed($values);
+ if (!$id) {
+ // There was an error in database... we cannot say what here.
+ throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+ }
+ $feed->_id($id);
+
+ // Ok, feed has been added in database. Now we have to refresh entries.
+ self::actualizeFeed($id, $url, false, null, true);
+
+ return $feed;
+ }
+
/**
* This action subscribes to a feed.
*
@@ -59,7 +115,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
$feedDAO = FreshRSS_Factory::createFeedDao();
- $this->catDAO = new FreshRSS_CategoryDAO();
$url_redirect = array(
'c' => 'subscription',
'a' => 'index',
@@ -74,26 +129,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
if (Minz_Request::isPost()) {
- @set_time_limit(300);
-
$cat = Minz_Request::param('category');
+ $new_cat_name = '';
if ($cat === 'nc') {
// User want to create a new category, new_category parameter
// must exist
$new_cat = Minz_Request::param('new_category');
- if (empty($new_cat['name'])) {
- $cat = false;
- } else {
- $cat = $this->catDAO->addCategory($new_cat);
- }
- }
-
- if ($cat === false) {
- // If category was not given or if creating new category failed,
- // get the default category
- $this->catDAO->checkDefault();
- $def_cat = $this->catDAO->getDefault();
- $cat = $def_cat->id();
+ $new_cat_name = isset($new_cat['name']) ? $new_cat['name'] : '';
}
// HTTP information are useful if feed is protected behind a
@@ -105,103 +147,24 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
$http_auth = $user . ':' . $pass;
}
- $transaction_started = false;
try {
- $feed = new FreshRSS_Feed($url);
+ $feed = self::addFeed($url, '', $cat, $new_cat_name, $http_auth);
} catch (FreshRSS_BadUrl_Exception $e) {
// Given url was not a valid url!
Minz_Log::warning($e->getMessage());
Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect);
- }
-
- $feed->_httpAuth($http_auth);
-
- try {
- $feed->load(true);
} catch (FreshRSS_Feed_Exception $e) {
// Something went bad (timeout, server not found, etc.)
Minz_Log::warning($e->getMessage());
- Minz_Request::bad(
- _t('feedback.sub.feed.internal_problem', _url('index', 'logs')),
- $url_redirect
- );
+ Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
} catch (Minz_FileNotExistException $e) {
// Cache directory doesn't exist!
Minz_Log::error($e->getMessage());
- Minz_Request::bad(
- _t('feedback.sub.feed.internal_problem', _url('index', 'logs')),
- $url_redirect
- );
- }
-
- if ($feedDAO->searchByUrl($feed->url())) {
- Minz_Request::bad(
- _t('feedback.sub.feed.already_subscribed', $feed->name()),
- $url_redirect
- );
- }
-
- $feed->_category($cat);
-
- // Call the extension hook
- $name = $feed->name();
- $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
- if ($feed === null) {
- Minz_Request::bad(_t('feedback.sub.feed.not_added', $name), $url_redirect);
- }
-
- $values = array(
- 'url' => $feed->url(),
- 'category' => $feed->category(),
- 'name' => $feed->name(),
- 'website' => $feed->website(),
- 'description' => $feed->description(),
- 'lastUpdate' => time(),
- 'httpAuth' => $feed->httpAuth(),
- );
-
- $id = $feedDAO->addFeed($values);
- if (!$id) {
- // There was an error in database... we cannot say what here.
- Minz_Request::bad(_t('feedback.sub.feed.not_added', $feed->name()), $url_redirect);
- }
-
- // Ok, feed has been added in database. Now we have to refresh entries.
- $feed->_id($id);
- $feed->faviconPrepare();
- //$feed->pubSubHubbubPrepare(); //TODO: prepare PubSubHubbub already when adding the feed
-
- $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::$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
- // performances.
- $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 ($entry === null) {
- // An extension has returned a null value, there is nothing to insert.
- continue;
- }
-
- $values = $entry->toArray();
- $entryDAO->addEntry($values);
- }
- $feedDAO->updateLastUpdate($feed->id());
- if ($feedDAO->inTransaction()) {
- $feedDAO->commit();
+ Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
+ } catch (FreshRSS_AlreadySubscribed_Exception $e) {
+ Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect);
+ } catch (FreshRSS_FeedNotAdded_Exception $e) {
+ Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect);
}
// Entries are in DB, we redirect to feed configuration page.
@@ -211,6 +174,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
// GET request: we must ask confirmation to user before adding feed.
Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
+ $this->catDAO = new FreshRSS_CategoryDAO();
$this->view->categories = $this->catDAO->listCategories(false);
$this->view->feed = new FreshRSS_Feed($url);
try {
@@ -261,38 +225,23 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
}
- /**
- * This action actualizes entries from one or several feeds.
- *
- * Parameters are:
- * - id (default: false): Feed ID
- * - url (default: false): Feed URL
- * - force (default: false)
- * 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($simplePiePush = null) {
+ public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) {
@set_time_limit(300);
$feedDAO = FreshRSS_Factory::createFeedDao();
$entryDAO = FreshRSS_Factory::createEntryDao();
- 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
+ // If feed_id is set and valid, corresponding feed is added to the list but
// alone in order to automatize further process.
$feeds = array();
- if ($id || $url) {
- $feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url);
+ if ($feed_id > 0 || $feed_url) {
+ $feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url);
if ($feed) {
$feeds[] = $feed;
}
} else {
- $feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
+ $feeds = $feedDAO->listFeedsOrderUpdate(-1);
}
// Calculate date of oldest entries we accept in DB.
@@ -309,13 +258,29 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
$url = $feed->url(); //For detection of HTTP 301
$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
- if ((!$simplePiePush) && (!$id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
+ if ((!$simplePiePush) && (!$feed_id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
//$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
//Minz_Log::debug($text);
//file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
continue; //When PubSubHubbub is used, do not pull refresh so often
}
+ $mtime = 0;
+ $ttl = $feed->ttl();
+ if ($ttl == -1) {
+ continue; //Feed refresh is disabled
+ }
+ if ((!$simplePiePush) && (!$feed_id) &&
+ ($feed->lastUpdate() + 10 >= time() - ($ttl == -2 ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) {
+ //Too early to refresh from source, but check whether the feed was updated by another user
+ $mtime = $feed->cacheModifiedTime();
+ if ($feed->lastUpdate() + 10 >= $mtime) {
+ continue; //Nothing newer from other users
+ }
+ //Minz_Log::debug($feed->url() . ' was updated at ' . date('c', $mtime) . ' by another user');
+ //Will take advantage of the newer cache
+ }
+
if (!$feed->lock()) {
Minz_Log::notice('Feed already being actualized: ' . $feed->url());
continue;
@@ -325,7 +290,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
if ($simplePiePush) {
$feed->loadEntries($simplePiePush); //Used by PubSubHubbub
} else {
- $feed->load(false);
+ $feed->load(false, $isNewFeed);
}
} catch (FreshRSS_Feed_Exception $e) {
Minz_Log::warning($e->getMessage());
@@ -335,7 +300,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
$feed_history = $feed->keepHistory();
- if ($feed_history == -2) {
+ if ($isNewFeed) {
+ $feed_history = -1; //∞
+ } elseif ($feed_history == -2) {
// TODO: -2 must be a constant!
// -2 means we take the default value from configuration
$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
@@ -346,7 +313,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
if (count($entries) > 0) {
$newGuids = array();
foreach ($entries as $entry) {
- $newGuids[] = $entry->guid();
+ $newGuids[] = safe_ascii($entry->guid());
}
// For this feed, check existing GUIDs already in database.
$existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
@@ -375,7 +342,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
// This entry should not be added considering configuration and date.
$oldGuids[] = $entry->guid();
} else {
- if ($entry_date < $date_min) {
+ if ($isNewFeed) {
+ $id = min(time(), $entry_date) . uSecString();
+ } elseif ($entry_date < $date_min) {
$id = min(time(), $entry_date) . uSecString();
$entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read
} else {
@@ -404,7 +373,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
$entryDAO->addEntry($entry->toArray());
}
}
- $entryDAO->updateLastSeen($feed->id(), $oldGuids);
+ $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime);
}
if ($feed_history >= 0 && rand(0, 30) === 1) {
@@ -423,7 +392,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
}
- $feedDAO->updateLastUpdate($feed->id(), 0, $entryDAO->inTransaction());
+ $feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime);
if ($entryDAO->inTransaction()) {
$entryDAO->commit();
}
@@ -464,6 +433,26 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
break;
}
}
+ return array($updated_feeds, reset($feeds));
+ }
+
+ /**
+ * This action actualizes entries from one or several feeds.
+ *
+ * Parameters are:
+ * - id (default: false): Feed ID
+ * - url (default: false): Feed URL
+ * - force (default: false)
+ * 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() {
+ Minz_Session::_param('actualize_feeds', false);
+ $id = Minz_Request::param('id');
+ $url = Minz_Request::param('url');
+ $force = Minz_Request::param('force');
+
+ list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force);
if (Minz_Request::param('ajax')) {
// Most of the time, ajax request is for only one feed. But since
@@ -479,7 +468,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
} else {
// 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())
));
@@ -492,6 +480,36 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
return $updated_feeds;
}
+ public static function renameFeed($feed_id, $feed_name) {
+ if ($feed_id <= 0 || $feed_name == '') {
+ return false;
+ }
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ return $feedDAO->updateFeed($feed_id, array('name' => $feed_name));
+ }
+
+ public static function moveFeed($feed_id, $cat_id, $new_cat_name = '') {
+ if ($feed_id <= 0 || ($cat_id <= 0 && $new_cat_name == '')) {
+ return false;
+ }
+
+ $catDAO = new FreshRSS_CategoryDAO();
+ if ($cat_id > 0) {
+ $cat = $catDAO->searchById($cat_id);
+ $cat_id = $cat == null ? 0 : $cat->id();
+ }
+ if ($cat_id <= 1 && $new_cat_name != '') {
+ $cat_id = $catDAO->addCategory(array('name' => $new_cat_name));
+ }
+ if ($cat_id <= 1) {
+ $catDAO->checkDefault();
+ $cat_id = FreshRSS_CategoryDAO::defaultCategoryId;
+ }
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ return $feedDAO->updateFeed($feed_id, array('category' => $cat_id));
+ }
+
/**
* This action changes the category of a feed.
*
@@ -512,20 +530,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
$feed_id = Minz_Request::param('f_id');
$cat_id = Minz_Request::param('c_id');
- if ($cat_id === false) {
- // If category was not given get the default one.
- $catDAO = new FreshRSS_CategoryDAO();
- $catDAO->checkDefault();
- $def_cat = $catDAO->getDefault();
- $cat_id = $def_cat->id();
- }
-
- $feedDAO = FreshRSS_Factory::createFeedDao();
- $values = array('category' => $cat_id);
-
- $feed = $feedDAO->searchById($feed_id);
- if ($feed && ($feed->category() == $cat_id ||
- $feedDAO->updateFeed($feed_id, $values))) {
+ if (self::moveFeed($feed_id, $cat_id)) {
// TODO: return something useful
} else {
Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
@@ -534,6 +539,21 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
}
}
+ public static function deleteFeed($feed_id) {
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ if ($feedDAO->deleteFeed($feed_id)) {
+ // TODO: Delete old favicon
+
+ // Remove related queries
+ FreshRSS_Context::$user_conf->queries = remove_query_by_get(
+ 'f_' . $feed_id, FreshRSS_Context::$user_conf->queries);
+ FreshRSS_Context::$user_conf->save();
+
+ return true;
+ }
+ return false;
+ }
+
/**
* This action deletes a feed.
*
@@ -552,21 +572,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
if (!$redirect_url) {
$redirect_url = array('c' => 'subscription', 'a' => 'index');
}
-
if (!Minz_Request::isPost()) {
Minz_Request::forward($redirect_url, true);
}
$id = Minz_Request::param('id');
- $feedDAO = FreshRSS_Factory::createFeedDao();
- if ($feedDAO->deleteFeed($id)) {
- // TODO: Delete old favicon
-
- // Remove related queries
- FreshRSS_Context::$user_conf->queries = remove_query_by_get(
- 'f_' . $id, FreshRSS_Context::$user_conf->queries);
- FreshRSS_Context::$user_conf->save();
+ if (self::deleteFeed($id)) {
Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url);
} else {
Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url);
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index 60e467255..3ba91a243 100644
--- a/app/Controllers/importExportController.php
+++ b/app/Controllers/importExportController.php
@@ -29,32 +29,14 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
Minz_View::prependTitle(_t('sub.import_export.title') . ' · ');
}
- /**
- * This action handles import action.
- *
- * It must be reached by a POST request.
- *
- * Parameter is:
- * - file (default: nothing!)
- * Available file types are: zip, json or xml.
- */
- public function importAction() {
- if (!Minz_Request::isPost()) {
- Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
- }
-
- $file = $_FILES['file'];
- $status_file = $file['error'];
-
- if ($status_file !== 0) {
- Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
- Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
- array('c' => 'importExport', 'a' => 'index'));
- }
+ public function importFile($name, $path, $username = null) {
+ require_once(LIB_PATH . '/lib_opml.php');
- @set_time_limit(300);
+ $this->catDAO = new FreshRSS_CategoryDAO($username);
+ $this->entryDAO = FreshRSS_Factory::createEntryDao($username);
+ $this->feedDAO = FreshRSS_Factory::createFeedDao($username);
- $type_file = $this->guessFileType($file['name']);
+ $type_file = self::guessFileType($name);
$list_files = array(
'opml' => array(),
@@ -65,21 +47,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
// We try to list all files according to their type
$list = array();
if ($type_file === 'zip' && extension_loaded('zip')) {
- $zip = zip_open($file['tmp_name']);
-
+ $zip = zip_open($path);
if (!is_resource($zip)) {
// zip_open cannot open file: something is wrong
- Minz_Log::warning('Zip archive cannot be imported. Error code: ' . $zip);
- Minz_Request::bad(_t('feedback.import_export.zip_error'),
- array('c' => 'importExport', 'a' => 'index'));
+ throw new FreshRSS_Zip_Exception($zip);
}
-
while (($zipfile = zip_read($zip)) !== false) {
if (!is_resource($zipfile)) {
// zip_entry() can also return an error code!
- Minz_Log::warning('Zip file cannot be imported. Error code: ' . $zipfile);
+ throw new FreshRSS_Zip_Exception($zipfile);
} else {
- $type_zipfile = $this->guessFileType(zip_entry_name($zipfile));
+ $type_zipfile = self::guessFileType(zip_entry_name($zipfile));
if ($type_file !== 'unknown') {
$list_files[$type_zipfile][] = zip_entry_read(
$zipfile,
@@ -88,29 +66,88 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
}
}
}
-
zip_close($zip);
} elseif ($type_file === 'zip') {
- // Zip extension is not loaded
- Minz_Request::bad(_t('feedback.import_export.no_zip_extension'),
- array('c' => 'importExport', 'a' => 'index'));
+ // ZIP extension is not loaded
+ throw new FreshRSS_ZipMissing_Exception();
} elseif ($type_file !== 'unknown') {
- $list_files[$type_file][] = file_get_contents($file['tmp_name']);
+ $list_files[$type_file][] = file_get_contents($path);
}
// Import file contents.
// OPML first(so categories and feeds are imported)
// Starred articles then so the "favourite" status is already set
// And finally all other files.
- $error = false;
+ $ok = true;
foreach ($list_files['opml'] as $opml_file) {
- $error = $this->importOpml($opml_file);
+ if (!$this->importOpml($opml_file)) {
+ $ok = false;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
+ } else {
+ Minz_Log::warning('Error during OPML import');
+ }
+ }
}
foreach ($list_files['json_starred'] as $article_file) {
- $error = $this->importJson($article_file, true);
+ if (!$this->importJson($article_file, true)) {
+ $ok = false;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during JSON stars import' . "\n");
+ } else {
+ Minz_Log::warning('Error during JSON stars import');
+ }
+ }
}
foreach ($list_files['json_feed'] as $article_file) {
- $error = $this->importJson($article_file);
+ if (!$this->importJson($article_file)) {
+ $ok = false;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during JSON feeds import' . "\n");
+ } else {
+ Minz_Log::warning('Error during JSON feeds import');
+ }
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * This action handles import action.
+ *
+ * It must be reached by a POST request.
+ *
+ * Parameter is:
+ * - file (default: nothing!)
+ * Available file types are: zip, json or xml.
+ */
+ public function importAction() {
+ if (!Minz_Request::isPost()) {
+ Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+ }
+
+ $file = $_FILES['file'];
+ $status_file = $file['error'];
+
+ if ($status_file !== 0) {
+ Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
+ Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+
+ @set_time_limit(300);
+
+ $error = false;
+ try {
+ $error = !$this->importFile($file['name'], $file['tmp_name']);
+ } catch (FreshRSS_ZipMissing_Exception $zme) {
+ Minz_Request::bad(_t('feedback.import_export.no_zip_extension'),
+ array('c' => 'importExport', 'a' => 'index'));
+ } catch (FreshRSS_Zip_Exception $ze) {
+ Minz_Log::warning('ZIP archive cannot be imported. Error code: ' . $ze->zipErrorCode());
+ Minz_Request::bad(_t('feedback.import_export.zip_error'),
+ array('c' => 'importExport', 'a' => 'index'));
}
// And finally, we get import status and redirect to the home page
@@ -126,7 +163,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* Itis a *very* basic guess file type function. Only based on filename.
* That's could be improved but should be enough for what we have to do.
*/
- private function guessFileType($filename) {
+ private static function guessFileType($filename) {
if (substr_compare($filename, '.zip', -4) === 0) {
return 'zip';
} elseif (substr_compare($filename, '.opml', -5) === 0 ||
@@ -146,15 +183,19 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* This method parses and imports an OPML file.
*
* @param string $opml_file the OPML file content.
- * @return boolean true if an error occured, false else.
+ * @return boolean false if an error occured, true otherwise.
*/
private function importOpml($opml_file) {
$opml_array = array();
try {
$opml_array = libopml_parse_string($opml_file, false);
} catch (LibOPML_Exception $e) {
- Minz_Log::warning($e->getMessage());
- return true;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
+ } else {
+ Minz_Log::warning($e->getMessage());
+ }
+ return false;
}
$this->catDAO->checkDefault();
@@ -167,51 +208,49 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
*
* @param array $opml_elements an OPML element (body or outline).
* @param string $parent_cat the name of the parent category.
- * @return boolean true if an error occured, false else.
+ * @return boolean false if an error occured, true otherwise.
*/
private function addOpmlElements($opml_elements, $parent_cat = null) {
- $error = false;
+ $ok = true;
$nb_feeds = count($this->feedDAO->listFeeds());
$nb_cats = count($this->catDAO->listCategories(false));
$limits = FreshRSS_Context::$system_conf->limits;
foreach ($opml_elements as $elt) {
- $is_error = false;
if (isset($elt['xmlUrl'])) {
// If xmlUrl exists, it means it is a feed
- if ($nb_feeds >= $limits['max_feeds']) {
+ if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
Minz_Log::warning(_t('feedback.sub.feed.over_max',
- $limits['max_feeds']));
- $is_error = true;
+ $limits['max_feeds']));
+ $ok = false;
continue;
}
- $is_error = $this->addFeedOpml($elt, $parent_cat);
- if (!$is_error) {
- $nb_feeds += 1;
+ if ($this->addFeedOpml($elt, $parent_cat)) {
+ $nb_feeds++;
+ } else {
+ $ok = false;
}
} else {
// No xmlUrl? It should be a category!
$limit_reached = ($nb_cats >= $limits['max_categories']);
- if ($limit_reached) {
+ if (!FreshRSS_Context::$isCli && $limit_reached) {
Minz_Log::warning(_t('feedback.sub.category.over_max',
- $limits['max_categories']));
+ $limits['max_categories']));
+ $ok = false;
+ continue;
}
- $is_error = $this->addCategoryOpml($elt, $parent_cat, $limit_reached);
- if (!$is_error) {
- $nb_cats += 1;
+ if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) {
+ $nb_cats++;
+ } else {
+ $ok = false;
}
}
-
- if (!$error && $is_error) {
- // oops: there is at least one error!
- $error = $is_error;
- }
}
- return $error;
+ return $ok;
}
/**
@@ -219,21 +258,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
*
* @param array $feed_elt an OPML element (must be a feed element).
* @param string $parent_cat the name of the parent category.
- * @return boolean true if an error occured, false else.
+ * @return boolean false if an error occured, true otherwise.
*/
private function addFeedOpml($feed_elt, $parent_cat) {
- $default_cat = $this->catDAO->getDefault();
- if (is_null($parent_cat)) {
+ if ($parent_cat == null) {
// This feed has no parent category so we get the default one
+ $this->catDAO->checkDefault();
+ $default_cat = $this->catDAO->getDefault();
$parent_cat = $default_cat->name();
}
$cat = $this->catDAO->searchByName($parent_cat);
- if (is_null($cat)) {
+ if ($cat == null) {
// If there is not $cat, it means parent category does not exist in
// database.
// If it happens, take the default category.
- $cat = $default_cat;
+ $this->catDAO->checkDefault();
+ $cat = $this->catDAO->getDefault();
}
// We get different useful information
@@ -259,7 +300,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
// Call the extension hook
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
- if (!is_null($feed)) {
+ if ($feed != null) {
// addFeedObject checks if feed is already in DB so nothing else to
// check here
$id = $this->feedDAO->addFeedObject($feed);
@@ -268,11 +309,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
$error = true;
}
} catch (FreshRSS_Feed_Exception $e) {
- Minz_Log::warning($e->getMessage());
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
+ } else {
+ Minz_Log::warning($e->getMessage());
+ }
$error = true;
}
- return $error;
+ if ($error) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n");
+ } else {
+ Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id());
+ }
+ }
+
+ return !$error;
}
/**
@@ -282,29 +335,34 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* @param string $parent_cat the name of the parent category.
* @param boolean $cat_limit_reached indicates if category limit has been reached.
* if yes, category is not added (but we try for feeds!)
- * @return boolean true if an error occured, false else.
+ * @return boolean false if an error occured, true otherwise.
*/
private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
// Create a new Category object
- $cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text']));
+ $catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
+ $cat = new FreshRSS_Category($catName);
$error = true;
- if (!$cat_limit_reached) {
+ if (FreshRSS_Context::$isCli || !$cat_limit_reached) {
$id = $this->catDAO->addCategoryObject($cat);
$error = ($id === false);
}
+ if ($error) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
+ } else {
+ Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
+ }
+ }
if (isset($cat_elt['@outlines'])) {
// Our cat_elt contains more categories or more feeds, so we
// add them recursively.
// Note: FreshRSS does not support yet category arborescence
- $res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name());
- if (!$error && $res) {
- $error = true;
- }
+ $error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName);
}
- return $error;
+ return !$error;
}
/**
@@ -312,13 +370,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
*
* @param string $article_file the JSON file content.
* @param boolean $starred true if articles from the file must be starred.
- * @return boolean true if an error occured, false else.
+ * @return boolean false if an error occured, true otherwise.
*/
private function importJson($article_file, $starred = false) {
$article_object = json_decode($article_file, true);
- if (is_null($article_object)) {
- Minz_Log::warning('Try to import a non-JSON file');
- return true;
+ if ($article_object == null) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n");
+ } else {
+ Minz_Log::warning('Try to import a non-JSON file');
+ }
+ return false;
}
$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
@@ -337,29 +399,36 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
$feed = new FreshRSS_Feed($item['origin'][$key]);
$feed = $this->feedDAO->searchByUrl($feed->url());
- if (is_null($feed)) {
+ if ($feed == null) {
// Feed does not exist in DB,we should to try to add it.
- if ($nb_feeds >= $limits['max_feeds']) {
+ if ((!FreshRSS_Context::$isCli) && ($nb_feeds >= $limits['max_feeds'])) {
// Oops, no more place!
Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds']));
} else {
$feed = $this->addFeedJson($item['origin'], $google_compliant);
}
- if (is_null($feed)) {
+ if ($feed == null) {
// Still null? It means something went wrong.
$error = true;
} else {
- // Nice! Increase the counter.
- $nb_feeds += 1;
+ $nb_feeds++;
}
}
- if (!is_null($feed)) {
+ if ($feed != null) {
$article_to_feed[$item['id']] = $feed->id();
}
}
+ $newGuids = array();
+ foreach ($article_object['items'] as $item) {
+ $newGuids[] = safe_ascii($item['id']);
+ }
+ // For this feed, check existing GUIDs already in database.
+ $existingHashForGuids = $this->entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
+ unset($newGuids);
+
// Then, articles are imported.
$this->entryDAO->beginTransaction();
foreach ($article_object['items'] as $item) {
@@ -376,7 +445,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
if ($google_compliant) {
// Remove tags containing "/state/com.google" which are useless.
$tags = array_filter($tags, function($var) {
- return strpos($var, '/state/com.google') === false;
+ return strpos($var, '/state/com.google') !== false;
});
}
@@ -389,13 +458,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
$entry->_tags($tags);
$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
- if (is_null($entry)) {
+ if ($entry == null) {
// An extension has returned a null value, there is nothing to insert.
continue;
}
$values = $entry->toArray();
- $id = $this->entryDAO->addEntry($values);
+ if (isset($existingHashForGuids[$entry->guid()])) {
+ $id = $this->entryDAO->updateEntry($values);
+ } else {
+ $id = $this->entryDAO->addEntry($values);
+ }
if (!$error && ($id === false)) {
$error = true;
@@ -403,7 +476,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
}
$this->entryDAO->commit();
- return $error;
+ return !$error;
}
/**
@@ -415,8 +488,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* else null.
*/
private function addFeedJson($origin, $google_compliant) {
- $default_cat = $this->catDAO->getDefault();
-
$return = null;
$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
$url = $origin[$key];
@@ -426,13 +497,13 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
try {
// Create a Feed object and add it in database.
$feed = new FreshRSS_Feed($url);
- $feed->_category($default_cat->id());
+ $feed->_category(FreshRSS_CategoryDAO::defaultCategoryId);
$feed->_name($name);
$feed->_website($website);
// Call the extension hook
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
- if (!is_null($feed)) {
+ if ($feed != null) {
// addFeedObject checks if feed is already in DB so nothing else to
// check here.
$id = $this->feedDAO->addFeedObject($feed);
@@ -443,67 +514,98 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
}
}
} catch (FreshRSS_Feed_Exception $e) {
- Minz_Log::warning($e->getMessage());
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during JSON feed import: ' . $e->getMessage() . "\n");
+ } else {
+ Minz_Log::warning($e->getMessage());
+ }
}
return $return;
}
- /**
- * This action handles export action.
- *
- * This action must be reached by a POST request.
- *
- * Parameters are:
- * - export_opml (default: false)
- * - export_starred (default: false)
- * - export_feeds (default: array()) a list of feed ids
- */
- public function exportAction() {
- if (!Minz_Request::isPost()) {
- Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
- }
+ public function exportFile($export_opml = true, $export_starred = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) {
+ require_once(LIB_PATH . '/lib_opml.php');
- $this->view->_useLayout(false);
+ $this->catDAO = new FreshRSS_CategoryDAO($username);
+ $this->entryDAO = FreshRSS_Factory::createEntryDao($username);
+ $this->feedDAO = FreshRSS_Factory::createFeedDao($username);
+
+ if ($export_feeds === true) {
+ //All feeds
+ $export_feeds = $this->feedDAO->listFeedsIds();
+ }
+ if (!is_array($export_feeds)) {
+ $export_feeds = array();
+ }
- $export_opml = Minz_Request::param('export_opml', false);
- $export_starred = Minz_Request::param('export_starred', false);
- $export_feeds = Minz_Request::param('export_feeds', array());
+ $day = date('Y-m-d');
$export_files = array();
if ($export_opml) {
- $export_files['feeds.opml'] = $this->generateOpml();
+ $export_files["feeds_${day}.opml.xml"] = $this->generateOpml();
}
if ($export_starred) {
- $export_files['starred.json'] = $this->generateEntries('starred');
+ $export_files["starred_${day}.json"] = $this->generateEntries('starred');
}
foreach ($export_feeds as $feed_id) {
$feed = $this->feedDAO->searchById($feed_id);
if ($feed) {
- $filename = 'feed_' . $feed->category() . '_'
+ $filename = "feed_${day}_" . $feed->category() . '_'
. $feed->id() . '.json';
- $export_files[$filename] = $this->generateEntries('feed', $feed);
+ $export_files[$filename] = $this->generateEntries('feed', $feed, $maxFeedEntries);
}
}
$nb_files = count($export_files);
if ($nb_files > 1) {
- // If there are more than 1 file to export, we need a zip archive.
+ // If there are more than 1 file to export, we need a ZIP archive.
try {
- $this->exportZip($export_files);
+ $this->sendZip($export_files);
} catch (Exception $e) {
- # Oops, there is no Zip extension!
- Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'),
- array('c' => 'importExport', 'a' => 'index'));
+ throw new FreshRSS_ZipMissing_Exception($e);
}
} elseif ($nb_files === 1) {
// Only one file? Guess its type and export it.
$filename = key($export_files);
- $type = $this->guessFileType($filename);
- $this->exportFile('freshrss_' . $filename, $export_files[$filename], $type);
- } else {
+ $type = self::guessFileType($filename);
+ $this->sendFile('freshrss_' . $filename, $export_files[$filename], $type);
+ }
+ return $nb_files;
+ }
+
+ /**
+ * This action handles export action.
+ *
+ * This action must be reached by a POST request.
+ *
+ * Parameters are:
+ * - export_opml (default: false)
+ * - export_starred (default: false)
+ * - export_feeds (default: array()) a list of feed ids
+ */
+ public function exportAction() {
+ if (!Minz_Request::isPost()) {
+ Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+ }
+ $this->view->_useLayout(false);
+
+ $nb_files = 0;
+ try {
+ $nb_files = $this->exportFile(
+ Minz_Request::param('export_opml', false),
+ Minz_Request::param('export_starred', false),
+ Minz_Request::param('export_feeds', array())
+ );
+ } catch (FreshRSS_ZipMissing_Exception $zme) {
+ # Oops, there is no ZIP extension!
+ Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+
+ if ($nb_files < 1) {
// Nothing to do...
Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
}
@@ -532,7 +634,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* @param FreshRSS_Feed $feed feed of which we want to get entries.
* @return string the JSON file content.
*/
- private function generateEntries($type, $feed = NULL) {
+ private function generateEntries($type, $feed = NULL, $maxFeedEntries = 50) {
$this->view->categories = $this->catDAO->listCategories();
if ($type == 'starred') {
@@ -542,12 +644,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
$this->view->entries = $this->entryDAO->listWhere(
's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all']
);
- } elseif ($type == 'feed' && !is_null($feed)) {
+ } elseif ($type === 'feed' && $feed != null) {
$this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
$this->view->type = 'feed/' . $feed->id();
$this->view->entries = $this->entryDAO->listWhere(
'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
- FreshRSS_Context::$user_conf->posts_per_page
+ $maxFeedEntries
);
$this->view->feed = $feed;
}
@@ -561,7 +663,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* @param array $files list of files where key is filename and value the content.
* @throws Exception if Zip extension is not loaded.
*/
- private function exportZip($files) {
+ private function sendZip($files) {
if (!extension_loaded('zip')) {
throw new Exception();
}
@@ -579,7 +681,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
$zip->close();
header('Content-Type: application/zip');
header('Content-Length: ' . filesize($zip_file));
- header('Content-Disposition: attachment; filename="freshrss_export.zip"');
+ $day = date('Y-m-d');
+ header('Content-Disposition: attachment; filename="freshrss_' . $day . '_export.zip"');
readfile($zip_file);
unlink($zip_file);
}
@@ -592,16 +695,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* @param string $type the file type (opml, json_feed or json_starred).
* If equals to unknown, nothing happens.
*/
- private function exportFile($filename, $content, $type) {
+ private function sendFile($filename, $content, $type) {
if ($type === 'unknown') {
return;
}
$content_type = '';
if ($type === 'opml') {
- $content_type = "text/opml";
+ $content_type = 'application/xml';
} elseif ($type === 'json_feed' || $type === 'json_starred') {
- $content_type = "text/json";
+ $content_type = 'application/json';
}
header('Content-Type: ' . $content_type . '; charset=utf-8');
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 2332d225d..5ca147ff3 100755
--- a/app/Controllers/indexController.php
+++ b/app/Controllers/indexController.php
@@ -34,7 +34,9 @@ class FreshRSS_index_Controller extends Minz_ActionController {
$this->view->callbackBeforeContent = function($view) {
try {
+ FreshRSS_Context::$number++; //+1 for pagination
$entries = FreshRSS_index_Controller::listEntriesByContext();
+ FreshRSS_Context::$number--;
$nb_entries = count($entries);
if ($nb_entries > FreshRSS_Context::$number) {
@@ -154,8 +156,14 @@ class FreshRSS_index_Controller extends Minz_ActionController {
* - order (default: conf->sort_order)
* - nb (default: conf->posts_per_page)
* - next (default: empty string)
+ * - hours (default: 0)
*/
private function updateContext() {
+ if (empty(FreshRSS_Context::$categories)) {
+ $catDAO = new FreshRSS_CategoryDAO();
+ FreshRSS_Context::$categories = $catDAO->listCategories();
+ }
+
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites();
@@ -180,10 +188,14 @@ class FreshRSS_index_Controller extends Minz_ActionController {
FreshRSS_Context::$order = Minz_Request::param(
'order', FreshRSS_Context::$user_conf->sort_order
);
- FreshRSS_Context::$number = Minz_Request::param(
- 'nb', FreshRSS_Context::$user_conf->posts_per_page
- );
+ FreshRSS_Context::$number = intval(Minz_Request::param('nb', FreshRSS_Context::$user_conf->posts_per_page));
+ if (FreshRSS_Context::$number > FreshRSS_Context::$user_conf->max_posts_per_rss) {
+ FreshRSS_Context::$number = max(
+ FreshRSS_Context::$user_conf->max_posts_per_rss,
+ FreshRSS_Context::$user_conf->posts_per_page);
+ }
FreshRSS_Context::$first_id = Minz_Request::param('next', '');
+ FreshRSS_Context::$sinceHours = intval(Minz_Request::param('hours', 0));
}
/**
@@ -201,11 +213,31 @@ class FreshRSS_index_Controller extends Minz_ActionController {
$id = '';
}
- return $entryDAO->listWhere(
+ $limit = FreshRSS_Context::$number;
+
+ $date_min = 0;
+ if (FreshRSS_Context::$sinceHours) {
+ $date_min = time() - (FreshRSS_Context::$sinceHours * 3600);
+ $limit = FreshRSS_Context::$user_conf->max_posts_per_rss;
+ }
+
+ $entries = $entryDAO->listWhere(
$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
- FreshRSS_Context::$number + 1, FreshRSS_Context::$first_id,
- FreshRSS_Context::$search
+ $limit, FreshRSS_Context::$first_id,
+ FreshRSS_Context::$search, $date_min
);
+
+ if (FreshRSS_Context::$sinceHours && (count($entries) < FreshRSS_Context::$user_conf->min_posts_per_rss)) {
+ $date_min = 0;
+ $limit = FreshRSS_Context::$user_conf->min_posts_per_rss;
+ $entries = $entryDAO->listWhere(
+ $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
+ $limit, FreshRSS_Context::$first_id,
+ FreshRSS_Context::$search, $date_min
+ );
+ }
+
+ return $entries;
}
/**
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
index 4a597ae7d..5d1dee72c 100644
--- a/app/Controllers/statsController.php
+++ b/app/Controllers/statsController.php
@@ -18,6 +18,27 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
Minz_View::prependTitle(_t('admin.stats.title') . ' · ');
}
+ private function convertToSerie($data) {
+ $serie = array();
+
+ foreach ($data as $key => $value) {
+ $serie[] = array($key, $value);
+ }
+
+ return $serie;
+ }
+
+ private function convertToPieSerie($data) {
+ $serie = array();
+
+ foreach ($data as $value) {
+ $value['data'] = array(array(0, (int) $value['data']));
+ $serie[] = $value;
+ }
+
+ return $serie;
+ }
+
/**
* This action handles the statistic main page.
*
@@ -33,10 +54,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
$statsDAO = FreshRSS_Factory::createStatsDAO();
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();
+ $entryCount = $statsDAO->calculateEntryCount();
+ $this->view->count = $this->convertToSerie($entryCount);
+ $this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2);
+ $this->view->feedByCategory = $this->convertToPieSerie($statsDAO->calculateFeedByCategory());
+ $this->view->entryByCategory = $this->convertToPieSerie($statsDAO->calculateEntryByCategory());
$this->view->topFeed = $statsDAO->calculateTopFeed();
}
@@ -118,11 +140,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
$this->view->days = $statsDAO->getDays();
$this->view->months = $statsDAO->getMonths();
$this->view->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id);
- $this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id);
+ $this->view->repartitionHour = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerHour($id));
$this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id);
- $this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id);
+ $this->view->repartitionDayOfWeek = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id));
$this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id);
- $this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id);
+ $this->view->repartitionMonth = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerMonth($id));
$this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id);
}
}
diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php
index 0521bc008..9d6ae18e6 100644
--- a/app/Controllers/userController.php
+++ b/app/Controllers/userController.php
@@ -24,6 +24,16 @@ class FreshRSS_user_Controller extends Minz_ActionController {
}
}
+ public static function hashPassword($passwordPlain) {
+ if (!function_exists('password_hash')) {
+ include_once(LIB_PATH . '/password_compat.php');
+ }
+ $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
+ $passwordPlain = '';
+ $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
+ return $passwordHash == '' ? '' : $passwordHash;
+ }
+
/**
* This action displays the user profile page.
*/
@@ -41,12 +51,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
if ($passwordPlain != '') {
Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP
$_POST['newPasswordPlain'] = '';
- if (!function_exists('password_hash')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
- $passwordPlain = '';
- $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
+ $passwordHash = self::hashPassword($passwordPlain);
$ok &= ($passwordHash != '');
FreshRSS_Context::$user_conf->passwordHash = $passwordHash;
}
@@ -54,12 +59,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
$passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
if ($passwordPlain != '') {
- if (!function_exists('password_hash')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
- $passwordPlain = '';
- $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
+ $passwordHash = self::hashPassword($passwordPlain);
$ok &= ($passwordHash != '');
FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash;
}
@@ -99,6 +99,50 @@ class FreshRSS_user_Controller extends Minz_ActionController {
$this->view->size_user = $entryDAO->size();
}
+ public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) {
+ if (!is_array($userConfig)) {
+ $userConfig = array();
+ }
+
+ $ok = ($new_user_name != '') && ctype_alnum($new_user_name);
+
+ if ($ok) {
+ $languages = Minz_Translate::availableLanguages();
+ if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages)) {
+ $userConfig['language'] = 'en';
+ }
+
+ $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive
+
+ $configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php');
+ $ok &= !file_exists($configPath);
+ }
+ if ($ok) {
+ $passwordHash = '';
+ if ($passwordPlain != '') {
+ $passwordHash = self::hashPassword($passwordPlain);
+ $ok &= ($passwordHash != '');
+ }
+
+ $apiPasswordHash = '';
+ if ($apiPasswordPlain != '') {
+ $apiPasswordHash = self::hashPassword($apiPasswordPlain);
+ $ok &= ($apiPasswordHash != '');
+ }
+ }
+ if ($ok) {
+ mkdir(join_path(DATA_PATH, 'users', $new_user_name));
+ $userConfig['passwordHash'] = $passwordHash;
+ $userConfig['apiPasswordHash'] = $apiPasswordHash;
+ $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
+ }
+ if ($ok) {
+ $userDAO = new FreshRSS_UserDAO();
+ $ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
+ }
+ return $ok;
+ }
+
/**
* This action creates a new user.
*
@@ -116,57 +160,13 @@ class FreshRSS_user_Controller extends Minz_ActionController {
FreshRSS_Auth::hasAccess('admin') ||
!max_registrations_reached()
)) {
- $db = FreshRSS_Context::$system_conf->db;
- require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
-
- $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
- $languages = Minz_Translate::availableLanguages();
- if (!isset($languages[$new_user_language])) {
- $new_user_language = FreshRSS_Context::$user_conf->language;
- }
-
$new_user_name = Minz_Request::param('new_user_name');
- $ok = ($new_user_name != '') && ctype_alnum($new_user_name);
-
- if ($ok) {
- $default_user = FreshRSS_Context::$system_conf->default_user;
- $ok &= (strcasecmp($new_user_name, $default_user) !== 0); //It is forbidden to alter the default user
-
- $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive
+ $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
+ $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
- $configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php');
- $ok &= !file_exists($configPath);
- }
- if ($ok) {
- $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
- $passwordHash = '';
- if ($passwordPlain != '') {
- Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
- $_POST['new_user_passwordPlain'] = '';
- if (!function_exists('password_hash')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
- $passwordPlain = '';
- $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
- $ok &= ($passwordHash != '');
- }
- if (empty($passwordHash)) {
- $passwordHash = '';
- }
- }
- if ($ok) {
- mkdir(join_path(DATA_PATH, 'users', $new_user_name));
- $config_array = array(
- 'language' => $new_user_language,
- 'passwordHash' => $passwordHash,
- );
- $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($config_array, true) . ';') !== false);
- }
- if ($ok) {
- $userDAO = new FreshRSS_UserDAO();
- $ok &= $userDAO->createUser($new_user_name);
- }
+ $ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language));
+ Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
+ $_POST['new_user_passwordPlain'] = '';
invalidateHttpCache();
$notif = array(
@@ -183,6 +183,27 @@ class FreshRSS_user_Controller extends Minz_ActionController {
Minz_Request::forward($redirect_url, true);
}
+ public static function deleteUser($username) {
+ $db = FreshRSS_Context::$system_conf->db;
+ require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+ $ok = ctype_alnum($username);
+ if ($ok) {
+ $default_user = FreshRSS_Context::$system_conf->default_user;
+ $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user
+ }
+ $user_data = join_path(DATA_PATH, 'users', $username);
+ if ($ok) {
+ $ok &= is_dir($user_data);
+ }
+ if ($ok) {
+ $userDAO = new FreshRSS_UserDAO();
+ $ok &= $userDAO->deleteUser($username);
+ $ok &= recursive_unlink($user_data);
+ }
+ return $ok;
+ }
+
/**
* This action delete an existing user.
*
@@ -204,16 +225,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
FreshRSS_Auth::hasAccess('admin') ||
$self_deletion
)) {
- $db = FreshRSS_Context::$system_conf->db;
- require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
-
- $ok = ctype_alnum($username);
- $user_data = join_path(DATA_PATH, 'users', $username);
-
- if ($ok) {
- $default_user = FreshRSS_Context::$system_conf->default_user;
- $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user
- }
+ $ok = true;
if ($ok && $self_deletion) {
// We check the password if it's a self-destruction
$nonce = Minz_Session::param('nonce');
@@ -225,12 +237,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
);
}
if ($ok) {
- $ok &= is_dir($user_data);
- }
- if ($ok) {
- $userDAO = new FreshRSS_UserDAO();
- $ok &= $userDAO->deleteUser($username);
- $ok &= recursive_unlink($user_data);
+ $ok &= self::deleteUser($username);
}
if ($ok && $self_deletion) {
FreshRSS_Auth::removeAccess();
diff --git a/app/Exceptions/AlreadySubscribedException.php b/app/Exceptions/AlreadySubscribedException.php
new file mode 100644
index 000000000..33b9f9555
--- /dev/null
+++ b/app/Exceptions/AlreadySubscribedException.php
@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_AlreadySubscribed_Exception extends Exception {
+ private $feedName = '';
+
+ public function __construct($url, $feedName) {
+ parent::__construct('Already subscribed! ' . $url, 2135);
+ $this->feedName = $feedName;
+ }
+
+ public function feedName() {
+ return $this->feedName;
+ }
+}
diff --git a/app/Exceptions/FeedNotAddedException.php b/app/Exceptions/FeedNotAddedException.php
new file mode 100644
index 000000000..350a17c56
--- /dev/null
+++ b/app/Exceptions/FeedNotAddedException.php
@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_FeedNotAdded_Exception extends Exception {
+ private $feedName = '';
+
+ public function __construct($url, $feedName) {
+ parent::__construct('Feed not added! ' . $url, 2147);
+ $this->feedName = $feedName;
+ }
+
+ public function feedName() {
+ return $this->feedName;
+ }
+}
diff --git a/app/Exceptions/ZipException.php b/app/Exceptions/ZipException.php
new file mode 100644
index 000000000..8441daedf
--- /dev/null
+++ b/app/Exceptions/ZipException.php
@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_Zip_Exception extends Exception {
+ private $zipErrorCode = 0;
+
+ public function __construct($zipErrorCode) {
+ parent::__construct('ZIP error! ' . $url, 2141);
+ $this->zipErrorCode = $zipErrorCode;
+ }
+
+ public function zipErrorCode() {
+ return $this->zipErrorCode;
+ }
+}
diff --git a/app/Exceptions/ZipMissingException.php b/app/Exceptions/ZipMissingException.php
new file mode 100644
index 000000000..864cc3991
--- /dev/null
+++ b/app/Exceptions/ZipMissingException.php
@@ -0,0 +1,4 @@
+<?php
+
+class FreshRSS_ZipMissing_Exception extends Exception {
+}
diff --git a/app/FreshRSS.php b/app/FreshRSS.php
index f9c371d27..e4caf23d1 100644
--- a/app/FreshRSS.php
+++ b/app/FreshRSS.php
@@ -49,7 +49,7 @@ class FreshRSS extends Minz_FrontController {
self::initI18n();
self::loadNotifications();
// Enable extensions for the current (logged) user.
- if (FreshRSS_Auth::hasAccess()) {
+ if (FreshRSS_Auth::hasAccess() || $system_conf->allow_anonymous) {
$ext_list = FreshRSS_Context::$user_conf->extensions_enabled;
Minz_ExtensionManager::enableByList($ext_list);
}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index fc431553e..c2d57c241 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -1,6 +1,9 @@
<?php
class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
+
+ const defaultCategoryId = 1;
+
public function addCategory($valuesTmp) {
$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)';
$stm = $this->bd->prepare($sql);
@@ -10,7 +13,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
);
if ($stm && $stm->execute($values)) {
- return $this->bd->lastInsertId();
+ return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error addCategory: ' . $info[2]);
@@ -50,6 +53,9 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
}
public function deleteCategory($id) {
+ if ($id <= self::defaultCategoryId) {
+ return false;
+ }
$sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?';
$stm = $this->bd->prepare($sql);
@@ -100,7 +106,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
public function listCategories($prePopulateFeeds = true, $details = false) {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
- . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ')
+ . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads` ')
. 'FROM `' . $this->prefix . 'category` c '
. 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
. 'GROUP BY f.id, c_id '
@@ -117,7 +123,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
}
public function getDefault() {
- $sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=1';
+ $sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=' . self::defaultCategoryId;
$stm = $this->bd->prepare($sql);
$stm->execute();
@@ -131,11 +137,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
}
}
public function checkDefault() {
- $def_cat = $this->searchById(1);
+ $def_cat = $this->searchById(self::defaultCategoryId);
if ($def_cat == null) {
$cat = new FreshRSS_Category(_t('gen.short.default_category'));
- $cat->_id(1);
+ $cat->_id(self::defaultCategoryId);
$values = array(
'id' => $cat->id(),
@@ -207,12 +213,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
$previousLine = null;
$feedsDao = array();
+ $feedDao = FreshRSS_Factory::createFeedDAO();
foreach ($listDAO as $line) {
if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
// End of the current category, we add it to the $list
$cat = new FreshRSS_Category(
$previousLine['c_name'],
- FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+ $feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
);
$cat->_id($previousLine['c_id']);
$list[$previousLine['c_id']] = $cat;
@@ -228,7 +235,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
if ($previousLine != null) {
$cat = new FreshRSS_Category(
$previousLine['c_name'],
- FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+ $feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
);
$cat->_id($previousLine['c_id']);
$list[$previousLine['c_id']] = $cat;
diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php
index 79bd0170b..046f54955 100644
--- a/app/Models/ConfigurationSetter.php
+++ b/app/Models/ConfigurationSetter.php
@@ -282,6 +282,7 @@ class FreshRSS_ConfigurationSetter {
switch ($value['type']) {
case 'mysql':
+ case 'pgsql':
if (empty($value['host']) ||
empty($value['user']) ||
empty($value['base']) ||
diff --git a/app/Models/Context.php b/app/Models/Context.php
index 2a58bd4ba..fd0e79fc1 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -35,6 +35,9 @@ class FreshRSS_Context {
public static $first_id = '';
public static $next_id = '';
public static $id_max = '';
+ public static $sinceHours = 0;
+
+ public static $isCli = false;
/**
* Initialize the context.
@@ -45,9 +48,6 @@ class FreshRSS_Context {
// Init configuration.
self::$system_conf = Minz_Configuration::get('system');
self::$user_conf = Minz_Configuration::get('user');
-
- $catDAO = new FreshRSS_CategoryDAO();
- self::$categories = $catDAO->listCategories();
}
/**
@@ -139,15 +139,22 @@ class FreshRSS_Context {
$id = substr($get, 2);
$nb_unread = 0;
+ if (empty(self::$categories)) {
+ $catDAO = new FreshRSS_CategoryDAO();
+ self::$categories = $catDAO->listCategories();
+ }
+
switch($type) {
case 'a':
self::$current_get['all'] = true;
self::$name = _t('index.feed.title');
+ self::$description = self::$system_conf->meta_description;
self::$get_unread = self::$total_unread;
break;
case 's':
self::$current_get['starred'] = true;
self::$name = _t('index.feed.title_fav');
+ self::$description = self::$system_conf->meta_description;
self::$get_unread = self::$total_starred['unread'];
// Update state if favorite is not yet enabled.
@@ -198,11 +205,16 @@ class FreshRSS_Context {
/**
* Set the value of $next_get attribute.
*/
- public static function _nextGet() {
+ private static function _nextGet() {
$get = self::currentGet();
// By default, $next_get == $get
self::$next_get = $get;
+ if (empty(self::$categories)) {
+ $catDAO = new FreshRSS_CategoryDAO();
+ self::$categories = $catDAO->listCategories();
+ }
+
if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
$another_unread_id = '';
$found_current_get = false;
diff --git a/app/Models/DatabaseDAOPGSQL.php b/app/Models/DatabaseDAOPGSQL.php
new file mode 100644
index 000000000..a4edaa448
--- /dev/null
+++ b/app/Models/DatabaseDAOPGSQL.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * This class is used to test database is well-constructed.
+ */
+class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
+ public function tablesAreCorrect() {
+ $db = FreshRSS_Context::$system_conf->db;
+ $dbowner = $db['user'];
+ $sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
+ $stm = $this->bd->prepare($sql);
+ $values = array($dbowner);
+ $stm->execute($values);
+ $res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+ $tables = array(
+ $this->prefix . 'category' => false,
+ $this->prefix . 'feed' => false,
+ $this->prefix . 'entry' => false,
+ );
+ foreach ($res as $value) {
+ $tables[array_pop($value)] = true;
+ }
+
+ return count(array_keys($tables, true, true)) == count($tables);
+ }
+
+ public function getSchema($table) {
+ $sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?';
+ $stm = $this->bd->prepare($sql);
+ $stm->execute(array($this->prefix . $table));
+ return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
+ }
+
+ public function daoToSchema($dao) {
+ return array(
+ 'name' => $dao['field'],
+ 'type' => strtolower($dao['type']),
+ 'notnull' => (bool)$dao['null'],
+ 'default' => $dao['default'],
+ );
+ }
+}
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 8f64098e5..4c6a9ea20 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -3,13 +3,21 @@
class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function isCompressed() {
- return parent::$sharedDbType !== 'sqlite';
+ return parent::$sharedDbType === 'mysql';
}
public function hasNativeHex() {
return parent::$sharedDbType !== 'sqlite';
}
+ public function sqlHexDecode($x) {
+ return 'unhex(' . $x . ')';
+ }
+
+ public function sqlHexEncode($x) {
+ return 'hex(' . $x . ')';
+ }
+
protected function addColumn($name) {
Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
$hasTransaction = false;
@@ -20,7 +28,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$this->bd->beginTransaction();
$hasTransaction = true;
}
- $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN lastSeen INT(11) DEFAULT 0');
+ $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()) {
@@ -105,32 +113,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($this->addEntryPrepared === null) {
$sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, '
. ($this->isCompressed() ? 'content_bin' : 'content')
- . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) '
- . 'VALUES(?, ?, ?, ?, '
- . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
- . ', ?, ?, ?, '
- . ($this->hasNativeHex() ? 'X?' : '?')
- . ', ?, ?, ?, ?)';
+ . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
+ . 'VALUES(:id, :guid, :title, :author, '
+ . ($this->isCompressed() ? 'COMPRESS(:content)' : ':content')
+ . ', :link, :date, :last_seen, '
+ . $this->sqlHexDecode(':hash')
+ . ', :is_read, :is_favorite, :id_feed, :tags)';
$this->addEntryPrepared = $this->bd->prepare($sql);
}
+ $this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
+ $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+ $valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
+ $this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+ $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+ $this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
+ $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+ $this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
+ $this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
+ $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+ $valuesTmp['link'] = safe_ascii($valuesTmp['link']);
+ $this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
+ $this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+ $valuesTmp['lastSeen'] = time();
+ $this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
+ $valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
+ $this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
+ $valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
+ $this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
+ $this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+ $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+ $this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+ if ($this->hasNativeHex()) {
+ $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+ } else {
+ $valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+
+ $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
+ }
- $values = array(
- $valuesTmp['id'],
- substr($valuesTmp['guid'], 0, 760),
- substr($valuesTmp['title'], 0, 255),
- substr($valuesTmp['author'], 0, 255),
- $valuesTmp['content'],
- substr($valuesTmp['link'], 0, 1023),
- $valuesTmp['date'],
- time(),
- $this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']), // X'09AF' hexadecimal literals do not work with SQLite/PDO //hex2bin() is PHP5.4+
- $valuesTmp['is_read'] ? 1 : 0,
- $valuesTmp['is_favorite'] ? 1 : 0,
- $valuesTmp['id_feed'],
- substr($valuesTmp['tags'], 0, 1023),
- );
-
- if ($this->addEntryPrepared && $this->addEntryPrepared->execute($values)) {
+ if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
return $this->bd->lastInsertId();
} else {
$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
@@ -153,35 +174,44 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($this->updateEntryPrepared === null) {
$sql = 'UPDATE `' . $this->prefix . 'entry` '
- . 'SET title=?, author=?, '
- . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?')
- . ', link=?, date=?, lastSeen=?, hash='
- . ($this->hasNativeHex() ? 'X?' : '?')
- . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ')
- . 'tags=? '
- . 'WHERE id_feed=? AND guid=?';
+ . 'SET title=:title, author=:author, '
+ . ($this->isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content')
+ . ', link=:link, date=:date, `lastSeen`=:last_seen, '
+ . 'hash=' . $this->sqlHexDecode(':hash')
+ . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=:is_read, ')
+ . 'tags=:tags '
+ . 'WHERE id_feed=:id_feed AND guid=:guid';
$this->updateEntryPrepared = $this->bd->prepare($sql);
}
- $values = array(
- substr($valuesTmp['title'], 0, 255),
- substr($valuesTmp['author'], 0, 255),
- $valuesTmp['content'],
- substr($valuesTmp['link'], 0, 1023),
- $valuesTmp['date'],
- time(),
- $this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),
- );
+ $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+ $this->updateEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+ $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+ $this->updateEntryPrepared->bindParam(':title', $valuesTmp['title']);
+ $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+ $this->updateEntryPrepared->bindParam(':author', $valuesTmp['author']);
+ $this->updateEntryPrepared->bindParam(':content', $valuesTmp['content']);
+ $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+ $valuesTmp['link'] = safe_ascii($valuesTmp['link']);
+ $this->updateEntryPrepared->bindParam(':link', $valuesTmp['link']);
+ $this->updateEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+ $valuesTmp['lastSeen'] = time();
+ $this->updateEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
if ($valuesTmp['is_read'] !== null) {
- $values[] = $valuesTmp['is_read'] ? 1 : 0;
+ $this->updateEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'] ? 1 : 0, PDO::PARAM_INT);
+ }
+ $this->updateEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+ $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+ $this->updateEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+ if ($this->hasNativeHex()) {
+ $this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+ } else {
+ $valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+
+ $this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
}
- $values = array_merge($values, array(
- substr($valuesTmp['tags'], 0, 1023),
- $valuesTmp['id_feed'],
- substr($valuesTmp['guid'], 0, 760),
- ));
- if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) {
+ if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
return $this->bd->lastInsertId();
} else {
$info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
@@ -246,15 +276,19 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. 'WHERE e.is_read=0 '
. 'GROUP BY e.id_feed'
. ') x ON x.id_feed=f.id '
- . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
- . 'WHERE 1';
+ . 'SET f.`cache_nbUnreads`=COALESCE(x.nbUnreads, 0)';
+ $hasWhere = false;
$values = array();
if ($feedId !== false) {
- $sql .= ' AND f.id=?';
+ $sql .= $hasWhere ? ' AND' : ' WHERE';
+ $hasWhere = true;
+ $sql .= ' f.id=?';
$values[] = $id;
}
if ($catId !== false) {
- $sql .= ' AND f.category=?';
+ $sql .= $hasWhere ? ' AND' : ' WHERE';
+ $hasWhere = true;
+ $sql .= ' f.category=?';
$values[] = $catId;
}
$stm = $this->bd->prepare($sql);
@@ -309,7 +343,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
} else {
$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
. 'SET e.is_read=?,'
- . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+ . 'f.`cache_nbUnreads`=f.`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
. 'WHERE e.id=? AND e.is_read=?';
$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
$stm = $this->bd->prepare($sql);
@@ -430,17 +464,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
$this->bd->beginTransaction();
- $sql = 'UPDATE `' . $this->prefix . 'entry` e '
- . 'SET e.is_read=1 '
- . 'WHERE e.id_feed=? AND e.is_read=0 AND e.id <= ?';
+ $sql = 'UPDATE `' . $this->prefix . 'entry` '
+ . 'SET is_read=1 '
+ . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
$values = array($id_feed, $idMax);
- list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+ list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
- Minz_Log::error('SQL error markReadFeed: ' . $info[2]);
+ Minz_Log::error('SQL error markReadFeed: ' . $info[2] . ' with SQL: ' . $sql . $search);
$this->bd->rollBack();
return false;
}
@@ -448,13 +482,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($affected > 0) {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
- . 'SET cache_nbUnreads=cache_nbUnreads-' . $affected
+ . 'SET `cache_nbUnreads`=`cache_nbUnreads`-' . $affected
. ' WHERE id=?';
$values = array($id_feed);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
- Minz_Log::error('SQL error markReadFeed: ' . $info[2]);
+ Minz_Log::error('SQL error markReadFeed cache: ' . $info[2]);
$this->bd->rollBack();
return false;
}
@@ -658,7 +692,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if (count($guids) < 1) {
return array();
}
- $sql = 'SELECT guid, hex(hash) AS hexHash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+ $guids = array_unique($guids);
+ $sql = 'SELECT guid, ' . $this->sqlHexEncode('hash') . ' AS hex_hash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
$stm = $this->bd->prepare($sql);
$values = array($id_feed);
$values = array_merge($values, $guids);
@@ -666,7 +701,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$result = array();
$rows = $stm->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
- $result[$row['guid']] = $row['hexHash'];
+ $result[$row['guid']] = $row['hex_hash'];
}
return $result;
} else {
@@ -680,13 +715,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
- public function updateLastSeen($id_feed, $guids) {
+ public function updateLastSeen($id_feed, $guids, $mtime = 0) {
if (count($guids) < 1) {
return 0;
}
- $sql = 'UPDATE `' . $this->prefix . 'entry` SET lastSeen=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+ $sql = 'UPDATE `' . $this->prefix . 'entry` SET `lastSeen`=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
$stm = $this->bd->prepare($sql);
- $values = array(time(), $id_feed);
+ if ($mtime <= 0) {
+ $mtime = time();
+ }
+ $values = array($mtime, $id_feed);
$values = array_merge($values, $guids);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php
new file mode 100644
index 000000000..b96a62ebc
--- /dev/null
+++ b/app/Models/EntryDAOPGSQL.php
@@ -0,0 +1,31 @@
+<?php
+
+class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
+
+ public function sqlHexDecode($x) {
+ return 'decode(' . $x . ", 'hex')";
+ }
+
+ public function sqlHexEncode($x) {
+ return 'encode(' . $x . ", 'hex')";
+ }
+
+ protected function autoUpdateDb($errorInfo) {
+ return false;
+ }
+
+ protected function addColumn($name) {
+ return false;
+ }
+
+ public function size($all = true) {
+ $db = FreshRSS_Context::$system_conf->db;
+ $sql = 'SELECT pg_size_pretty(pg_database_size(?))';
+ $values = array($db['base']);
+ $stm = $this->bd->prepare($sql);
+ $stm->execute($values);
+ $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+ return $res[0];
+ }
+
+}
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index dad34a93d..34e854608 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -2,6 +2,10 @@
class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
+ public function sqlHexDecode($x) {
+ return $x;
+ }
+
protected function autoUpdateDb($errorInfo) {
if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR
//autoAddColumn
@@ -24,17 +28,21 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
protected function updateCacheUnreads($catId = false, $feedId = false) {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
- . 'SET cache_nbUnreads=('
+ . 'SET `cache_nbUnreads`=('
. 'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
- . 'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0) '
- . 'WHERE 1';
+ . 'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0)';
+ $hasWhere = false;
$values = array();
if ($feedId !== false) {
- $sql .= ' AND id=?';
+ $sql .= $hasWhere ? ' AND' : ' WHERE';
+ $hasWhere = true;
+ $sql .= ' id=?';
$values[] = $feedId;
}
if ($catId !== false) {
- $sql .= ' AND category=?';
+ $sql .= $hasWhere ? ' AND' : ' WHERE';
+ $hasWhere = true;
+ $sql .= ' category=?';
$values[] = $catId;
}
$stm = $this->bd->prepare($sql);
@@ -82,7 +90,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
}
$affected = $stm->rowCount();
if ($affected > 0) {
- $sql = 'UPDATE `' . $this->prefix . 'feed` SET cache_nbUnreads=cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+ $sql = 'UPDATE `' . $this->prefix . 'feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
. 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
$values = array($ids);
$stm = $this->bd->prepare($sql);
diff --git a/app/Models/Factory.php b/app/Models/Factory.php
index db09d155d..764987c46 100644
--- a/app/Models/Factory.php
+++ b/app/Models/Factory.php
@@ -4,37 +4,47 @@ class FreshRSS_Factory {
public static function createFeedDao($username = null) {
$conf = Minz_Configuration::get('system');
- if ($conf->db['type'] === 'sqlite') {
- return new FreshRSS_FeedDAOSQLite($username);
- } else {
- return new FreshRSS_FeedDAO($username);
+ switch ($conf->db['type']) {
+ case 'sqlite':
+ return new FreshRSS_FeedDAOSQLite($username);
+ default:
+ return new FreshRSS_FeedDAO($username);
}
}
public static function createEntryDao($username = null) {
$conf = Minz_Configuration::get('system');
- if ($conf->db['type'] === 'sqlite') {
- return new FreshRSS_EntryDAOSQLite($username);
- } else {
- return new FreshRSS_EntryDAO($username);
+ switch ($conf->db['type']) {
+ case 'sqlite':
+ return new FreshRSS_EntryDAOSQLite($username);
+ case 'pgsql':
+ return new FreshRSS_EntryDAOPGSQL($username);
+ default:
+ return new FreshRSS_EntryDAO($username);
}
}
public static function createStatsDAO($username = null) {
$conf = Minz_Configuration::get('system');
- if ($conf->db['type'] === 'sqlite') {
- return new FreshRSS_StatsDAOSQLite($username);
- } else {
- return new FreshRSS_StatsDAO($username);
+ switch ($conf->db['type']) {
+ case 'sqlite':
+ return new FreshRSS_StatsDAOSQLite($username);
+ case 'pgsql':
+ return new FreshRSS_StatsDAOPGSQL($username);
+ default:
+ return new FreshRSS_StatsDAO($username);
}
}
public static function createDatabaseDAO($username = null) {
$conf = Minz_Configuration::get('system');
- if ($conf->db['type'] === 'sqlite') {
- return new FreshRSS_DatabaseDAOSQLite($username);
- } else {
- return new FreshRSS_DatabaseDAO($username);
+ switch ($conf->db['type']) {
+ case 'sqlite':
+ return new FreshRSS_DatabaseDAOSQLite($username);
+ case 'pgsql':
+ return new FreshRSS_DatabaseDAOPGSQL($username);
+ default:
+ return new FreshRSS_DatabaseDAO($username);
}
}
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 6104b1e31..97cb1c47e 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -131,13 +131,26 @@ class FreshRSS_Feed extends Minz_Model {
return $this->nbNotRead;
}
public function faviconPrepare() {
- $file = DATA_PATH . '/favicons/' . $this->hash() . '.txt';
- if (!file_exists($file)) {
- $t = $this->website;
- if ($t == '') {
- $t = $this->url;
+ global $favicons_dir;
+ require_once(LIB_PATH . '/favicons.php');
+ $url = $this->website;
+ if ($url == '') {
+ $url = $this->url;
+ }
+ $txt = $favicons_dir . $this->hash() . '.txt';
+ if (!file_exists($txt)) {
+ file_put_contents($txt, $url);
+ }
+ if (FreshRSS_Context::$isCli) {
+ $ico = $favicons_dir . $this->hash() . '.ico';
+ $ico_mtime = @filemtime($ico);
+ $txt_mtime = @filemtime($txt);
+ if ($txt_mtime != false &&
+ ($ico_mtime == false || $ico_mtime < $txt_mtime || ($ico_mtime < time() - (14 * 86400)))) {
+ // no ico file or we should download a new one.
+ $url = file_get_contents($txt);
+ download_favicon($url, $ico) || touch($ico);
}
- file_put_contents($file, $t);
}
}
public static function faviconDelete($hash) {
@@ -216,7 +229,7 @@ class FreshRSS_Feed extends Minz_Model {
$this->nbEntries = intval($value);
}
- public function load($loadDetails = false) {
+ public function load($loadDetails = false, $noCache = false) {
if ($this->url !== null) {
if (CACHE_PATH === false) {
throw new Minz_FileNotExistException(
@@ -268,7 +281,7 @@ class FreshRSS_Feed extends Minz_Model {
$this->_url($clean_url);
}
- if (($mtime === true) || ($mtime > $this->lastUpdate)) {
+ if (($mtime === true) || ($mtime > $this->lastUpdate) || $noCache) {
//Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
$this->loadEntries($feed); // et on charge les articles du flux
} else {
@@ -309,11 +322,11 @@ class FreshRSS_Feed extends Minz_Model {
$elinks[$elink] = '1';
$mime = strtolower($enclosure->get_type());
if (strpos($mime, 'image/') === 0) {
- $content .= '<br /><img lazyload="" postpone="" src="' . $elink . '" alt="" />';
+ $content .= '<p class="enclosure"><img src="' . $elink . '" alt="" /></p>';
} elseif (strpos($mime, 'audio/') === 0) {
- $content .= '<br /><audio lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
+ $content .= '<p class="enclosure"><audio preload="none" src="' . $elink . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
} elseif (strpos($mime, 'video/') === 0) {
- $content .= '<br /><video lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />';
+ $content .= '<p class="enclosure"><video preload="none" src="' . $elink . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
} else {
unset($elinks[$elink]);
}
@@ -340,6 +353,10 @@ class FreshRSS_Feed extends Minz_Model {
$this->entries = $entries;
}
+ function cacheModifiedTime() {
+ return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc');
+ }
+
function lock() {
$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
@@ -460,7 +477,7 @@ class FreshRSS_Feed extends Minz_Model {
CURLOPT_URL => $this->hubUrl,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
- CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
+ CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
CURLOPT_POSTFIELDS => 'hub.verify=sync'
. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
. '&hub.topic=' . urlencode($this->selfUrl)
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 475d39286..68398efd5 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -2,9 +2,12 @@
class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function addFeed($valuesTmp) {
- $sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
+ $sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, `lastUpdate`, priority, `httpAuth`, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
$stm = $this->bd->prepare($sql);
+ $valuesTmp['url'] = safe_ascii($valuesTmp['url']);
+ $valuesTmp['website'] = safe_ascii($valuesTmp['website']);
+
$values = array(
substr($valuesTmp['url'], 0, 511),
$valuesTmp['category'],
@@ -16,7 +19,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
);
if ($stm && $stm->execute($values)) {
- return $this->bd->lastInsertId();
+ return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error addFeed: ' . $info[2]);
@@ -55,6 +58,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
public function updateFeed($id, $valuesTmp) {
+ if (isset($valuesTmp['url'])) {
+ $valuesTmp['url'] = safe_ascii($valuesTmp['url']);
+ }
+ if (isset($valuesTmp['website'])) {
+ $valuesTmp['website'] = safe_ascii($valuesTmp['website']);
+ }
+
$set = '';
foreach ($valuesTmp as $key => $v) {
$set .= $key . '=?, ';
@@ -82,22 +92,26 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
- public function updateLastUpdate($id, $inError = 0, $updateCache = true) {
+ public function updateLastUpdate($id, $inError = false, $updateCache = true, $mtime = 0) {
if ($updateCache) {
$sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
- . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
- . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
- . 'lastUpdate=?, error=? '
+ . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+ . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
+ . '`lastUpdate`=?, error=? '
. 'WHERE id=?';
} else {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
- . 'SET lastUpdate=?, error=? '
+ . 'SET `lastUpdate`=?, error=? '
. 'WHERE id=?';
}
+ if ($mtime <= 0) {
+ $mtime = time();
+ }
+
$values = array(
- time(),
- $inError,
+ $mtime,
+ $inError ? 1 : 0,
$id,
);
@@ -198,6 +212,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
+ public function listFeedsIds() {
+ $sql = 'SELECT id FROM `' . $this->prefix . 'feed`';
+ $stm = $this->bd->prepare($sql);
+ $stm->execute();
+ return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+ }
+
public function listFeeds() {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
$stm = $this->bd->prepare($sql);
@@ -222,14 +243,14 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $feedCategoryNames;
}
+ /**
+ * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
+ */
public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
- if ($defaultCacheDuration < 0) {
- $defaultCacheDuration = 2147483647;
- }
- $sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl '
+ $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl '
. 'FROM `' . $this->prefix . 'feed` '
- . 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) '
- . 'ORDER BY lastUpdate';
+ . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl <> -1 AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
+ . 'ORDER BY `lastUpdate`';
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute())) {
$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2'; //v0.7.3
@@ -282,7 +303,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. 'FROM `' . $this->prefix . 'entry` e '
. 'GROUP BY e.id_feed'
. ') x ON x.id_feed=f.id '
- . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
+ . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads';
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute()) {
@@ -308,7 +329,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$affected = $stm->rowCount();
$sql = 'UPDATE `' . $this->prefix . 'feed` '
- . 'SET cache_nbEntries=0, cache_nbUnreads=0 WHERE id=?';
+ . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=?';
$values = array($id);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
@@ -326,7 +347,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
. 'WHERE id_feed=:id_feed AND id<=:id_max '
. 'AND is_favorite=0 ' //Do not remove favourites
- . 'AND lastSeen < (SELECT maxLastSeen FROM (SELECT (MAX(e3.lastSeen)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance
+ . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance
. 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
$stm = $this->bd->prepare($sql);
diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php
index 7599fda53..440ae74da 100644
--- a/app/Models/FeedDAOSQLite.php
+++ b/app/Models/FeedDAOSQLite.php
@@ -4,8 +4,8 @@ class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
public function updateCachedValues() { //For one single feed, call updateLastUpdate($id)
$sql = 'UPDATE `' . $this->prefix . 'feed` '
- . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
- . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
+ . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+ . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute()) {
return $stm->rowCount();
diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php
index 4f83ff577..2ce4f2944 100644
--- a/app/Models/StatsDAO.php
+++ b/app/Models/StatsDAO.php
@@ -4,6 +4,10 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
const ENTRY_COUNT_PERIOD = 30;
+ protected function sqlFloor($s) {
+ return "FLOOR($s)";
+ }
+
/**
* Calculates entry repartition for all feeds and for main stream.
*
@@ -37,12 +41,12 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
$filter .= "AND e.id_feed = {$feed}";
}
$sql = <<<SQL
-SELECT COUNT(1) AS `total`,
-COUNT(1) - SUM(e.is_read) AS `unread`,
-SUM(e.is_read) AS `read`,
-SUM(e.is_favorite) AS `favorite`
-FROM {$this->prefix}entry AS e
-, {$this->prefix}feed AS f
+SELECT COUNT(1) AS total,
+COUNT(1) - SUM(e.is_read) AS count_unreads,
+SUM(e.is_read) AS count_reads,
+SUM(e.is_favorite) AS count_favorites
+FROM `{$this->prefix}entry` AS e
+, `{$this->prefix}feed` AS f
WHERE e.id_feed = f.id
{$filter}
SQL;
@@ -61,14 +65,16 @@ SQL;
*/
public function calculateEntryCount() {
$count = $this->initEntryCountArray();
- $period = self::ENTRY_COUNT_PERIOD;
+ $midnight = mktime(0, 0, 0);
+ $oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
// Get stats per day for the last 30 days
+ $sqlDay = $this->sqlFloor("(date - $midnight) / 86400");
$sql = <<<SQL
-SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day,
-COUNT(1) AS count
-FROM {$this->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')
+SELECT {$sqlDay} AS day,
+COUNT(*) as count
+FROM `{$this->prefix}entry`
+WHERE date >= {$oldest} AND date < {$midnight}
GROUP BY day
ORDER BY day ASC
SQL;
@@ -80,28 +86,7 @@ SQL;
$count[$value['day']] = (int) $value['count'];
}
- 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 = <<<SQL
-SELECT COUNT(1) / {$period} AS average
-FROM {$this->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);
+ return $count;
}
/**
@@ -158,7 +143,7 @@ SQL;
$sql = <<<SQL
SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
, COUNT(1) AS count
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
{$restrict}
GROUP BY period
ORDER BY period ASC
@@ -173,7 +158,7 @@ SQL;
$repartition[(int) $value['period']] = (int) $value['count'];
}
- return $this->convertToSerie($repartition);
+ return $repartition;
}
/**
@@ -222,7 +207,7 @@ SQL;
SELECT COUNT(1) AS count
, MIN(date) AS date_min
, MAX(date) AS date_max
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
{$restrict}
SQL;
$stm = $this->bd->prepare($sql);
@@ -266,8 +251,8 @@ SQL;
$sql = <<<SQL
SELECT c.name AS label
, COUNT(f.id) AS data
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f
WHERE c.id = f.category
GROUP BY label
ORDER BY data DESC
@@ -276,7 +261,7 @@ SQL;
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $this->convertToPieSerie($res);
+ return $res;
}
/**
@@ -289,9 +274,9 @@ SQL;
$sql = <<<SQL
SELECT c.name AS label
, COUNT(e.id) AS data
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
WHERE c.id = f.category
AND f.id = e.id_feed
GROUP BY label
@@ -301,7 +286,7 @@ SQL;
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $this->convertToPieSerie($res);
+ return $res;
}
/**
@@ -315,9 +300,9 @@ SELECT f.id AS id
, MAX(f.name) AS name
, MAX(c.name) AS category
, COUNT(e.id) AS count
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
WHERE c.id = f.category
AND f.id = e.id_feed
GROUP BY f.id
@@ -340,8 +325,8 @@ SELECT MAX(f.id) as id
, MAX(f.name) AS name
, MAX(date) AS last_date
, COUNT(*) AS nb_articles
-FROM {$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
WHERE f.id = e.id_feed
GROUP BY f.id
ORDER BY name
@@ -351,27 +336,6 @@ SQL;
return $stm->fetchAll(PDO::FETCH_ASSOC);
}
- protected function convertToSerie($data) {
- $serie = array();
-
- foreach ($data as $key => $value) {
- $serie[] = array($key, $value);
- }
-
- return $serie;
- }
-
- protected function convertToPieSerie($data) {
- $serie = array();
-
- foreach ($data as $value) {
- $value['data'] = array(array(0, (int) $value['data']));
- $serie[] = $value;
- }
-
- return $serie;
- }
-
/**
* Gets days ready for graphs
*
diff --git a/app/Models/StatsDAOPGSQL.php b/app/Models/StatsDAOPGSQL.php
new file mode 100644
index 000000000..1effbb64b
--- /dev/null
+++ b/app/Models/StatsDAOPGSQL.php
@@ -0,0 +1,67 @@
+<?php
+
+class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
+
+ /**
+ * Calculates the number of article per hour of the day per feed
+ *
+ * @param integer $feed id
+ * @return string
+ */
+ public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
+ return $this->calculateEntryRepartitionPerFeedPerPeriod('hour', $feed);
+ }
+
+ /**
+ * Calculates the number of article per day of week per feed
+ *
+ * @param integer $feed id
+ * @return string
+ */
+ public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
+ return $this->calculateEntryRepartitionPerFeedPerPeriod('day', $feed);
+ }
+
+ /**
+ * Calculates the number of article per month per feed
+ *
+ * @param integer $feed
+ * @return string
+ */
+ public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
+ return $this->calculateEntryRepartitionPerFeedPerPeriod('month', $feed);
+ }
+
+ /**
+ * Calculates the number of article per period per feed
+ *
+ * @param string $period format string to use for grouping
+ * @param integer $feed id
+ * @return string
+ */
+ protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+ $restrict = '';
+ if ($feed) {
+ $restrict = "WHERE e.id_feed = {$feed}";
+ }
+ $sql = <<<SQL
+SELECT extract( {$period} from to_timestamp(e.date)) AS period
+, COUNT(1) AS count
+FROM "{$this->prefix}entry" AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+ $stm = $this->bd->prepare($sql);
+ $stm->execute();
+ $res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+ foreach ($res as $value) {
+ $repartition[(int) $value['period']] = (int) $value['count'];
+ }
+
+ return $repartition;
+ }
+
+}
diff --git a/app/Models/StatsDAOSQLite.php b/app/Models/StatsDAOSQLite.php
index 9bfe8b20a..6cfc20463 100644
--- a/app/Models/StatsDAOSQLite.php
+++ b/app/Models/StatsDAOSQLite.php
@@ -2,59 +2,8 @@
class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
- /**
- * Calculates entry count per day on a 30 days period.
- * Returns the result as a JSON object.
- *
- * @return JSON object
- */
- public function calculateEntryCount() {
- $count = $this->initEntryCountArray();
- $period = parent::ENTRY_COUNT_PERIOD;
-
- // Get stats per day for the last 30 days
- $sql = <<<SQL
-SELECT round(julianday(e.date, 'unixepoch') - julianday('now')) AS day,
-COUNT(1) AS count
-FROM {$this->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')
-GROUP BY day
-ORDER BY day ASC
-SQL;
- $stm = $this->bd->prepare($sql);
- $stm->execute();
- $res = $stm->fetchAll(PDO::FETCH_ASSOC);
-
- foreach ($res as $value) {
- $count[(int) $value['day']] = (int) $value['count'];
- }
-
- 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 = <<<SQL
-SELECT COUNT(1) / {$period} AS average
-FROM {$this->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 sqlFloor($s) {
+ return "CAST(($s) AS INT)";
}
protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
@@ -66,7 +15,7 @@ SQL;
$sql = <<<SQL
SELECT strftime('{$period}', e.date, 'unixepoch') AS period
, COUNT(1) AS count
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
{$restrict}
GROUP BY period
ORDER BY period ASC
@@ -81,7 +30,7 @@ SQL;
$repartition[(int) $value['period']] = (int) $value['count'];
}
- return $this->convertToSerie($repartition);
+ return $repartition;
}
}
diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php
index b55766ab4..a95ee6bc4 100644
--- a/app/Models/UserDAO.php
+++ b/app/Models/UserDAO.php
@@ -1,34 +1,60 @@
<?php
class FreshRSS_UserDAO extends Minz_ModelPdo {
- public function createUser($username) {
+ public function createUser($username, $new_user_language, $insertDefaultFeeds = true) {
$db = FreshRSS_Context::$system_conf->db;
require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
$userPDO = new Minz_ModelPdo($username);
- $ok = false;
- if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL
- $sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', _t('gen.short.default_category'));
- $stm = $userPDO->bd->prepare($sql);
- $ok = $stm && $stm->execute();
- } else { //E.g. SQLite
- global $SQL_CREATE_TABLES;
- if (is_array($SQL_CREATE_TABLES)) {
- $ok = true;
- foreach ($SQL_CREATE_TABLES as $instruction) {
- $sql = sprintf($instruction, '', _t('gen.short.default_category'));
+ $currentLanguage = Minz_Translate::language();
+
+ try {
+ Minz_Translate::reset($new_user_language);
+ $ok = false;
+ $bd_prefix_user = $db['prefix'] . $username . '_';
+ if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL
+ $sql = sprintf(SQL_CREATE_TABLES, $bd_prefix_user, _t('gen.short.default_category'));
+ $stm = $userPDO->bd->prepare($sql);
+ $ok = $stm && $stm->execute();
+ } else { //E.g. SQLite
+ global $SQL_CREATE_TABLES;
+ if (is_array($SQL_CREATE_TABLES)) {
+ $ok = true;
+ foreach ($SQL_CREATE_TABLES as $instruction) {
+ $sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
+ $stm = $userPDO->bd->prepare($sql);
+ $ok &= ($stm && $stm->execute());
+ }
+ }
+ }
+ if ($insertDefaultFeeds) {
+ if (defined('SQL_INSERT_FEEDS')) { //E.g. MySQL
+ $sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
$stm = $userPDO->bd->prepare($sql);
- $ok &= ($stm && $stm->execute());
+ $ok &= $stm && $stm->execute();
+ } else { //E.g. SQLite
+ global $SQL_INSERT_FEEDS;
+ if (is_array($SQL_INSERT_FEEDS)) {
+ foreach ($SQL_INSERT_FEEDS as $instruction) {
+ $sql = sprintf($instruction, $bd_prefix_user);
+ $stm = $userPDO->bd->prepare($sql);
+ $ok &= ($stm && $stm->execute());
+ }
+ }
}
}
+ } catch (Exception $e) {
+ Minz_Log::error('Error while creating user: ' . $e->getMessage());
}
+ Minz_Translate::reset($currentLanguage);
+
if ($ok) {
return true;
} else {
$info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo();
- Minz_Log::error('SQL error : ' . $info[2]);
+ Minz_Log::error('SQL error: ' . $info[2]);
return false;
}
}
diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php
index c78839ef7..a454829d5 100644
--- a/app/SQL/install.sql.mysql.php
+++ b/app/SQL/install.sql.mysql.php
@@ -1,4 +1,6 @@
<?php
+define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+
define('SQL_CREATE_TABLES', '
CREATE TABLE IF NOT EXISTS `%1$scategory` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
@@ -57,11 +59,14 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` (
ENGINE = INNODB;
INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
+');
+
+define('SQL_INSERT_FEEDS', '
INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);
');
-define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentry`, `%1$sfeed`, `%1$scategory`');
define('SQL_UPDATE_UTF8MB4', '
ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php
new file mode 100644
index 000000000..9f4240b98
--- /dev/null
+++ b/app/SQL/install.sql.pgsql.php
@@ -0,0 +1,63 @@
+<?php
+define('SQL_CREATE_DB', 'CREATE DATABASE %1$s ENCODING \'UTF8\';');
+
+global $SQL_CREATE_TABLES;
+$SQL_CREATE_TABLES = array(
+'CREATE TABLE IF NOT EXISTS "%1$scategory" (
+ "id" SERIAL PRIMARY KEY,
+ "name" VARCHAR(255) UNIQUE NOT NULL
+);',
+
+'CREATE TABLE IF NOT EXISTS "%1$sfeed" (
+ "id" SERIAL PRIMARY KEY,
+ "url" varchar(511) UNIQUE NOT NULL,
+ "category" SMALLINT DEFAULT 0,
+ "name" VARCHAR(255) NOT NULL,
+ "website" VARCHAR(255),
+ "description" text,
+ "lastUpdate" INT DEFAULT 0,
+ "priority" SMALLINT NOT NULL DEFAULT 10,
+ "pathEntries" VARCHAR(511) DEFAULT NULL,
+ "httpAuth" VARCHAR(511) DEFAULT NULL,
+ "error" smallint DEFAULT 0,
+ "keep_history" INT NOT NULL DEFAULT -2,
+ "ttl" INT NOT NULL DEFAULT -2,
+ "cache_nbEntries" INT DEFAULT 0,
+ "cache_nbUnreads" INT DEFAULT 0,
+ FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);',
+'CREATE INDEX %1$sname_index ON "%1$sfeed" ("name");',
+'CREATE INDEX %1$spriority_index ON "%1$sfeed" ("priority");',
+'CREATE INDEX %1$skeep_history_index ON "%1$sfeed" ("keep_history");',
+
+'CREATE TABLE IF NOT EXISTS "%1$sentry" (
+ "id" BIGINT NOT NULL PRIMARY KEY,
+ "guid" VARCHAR(760) UNIQUE NOT NULL,
+ "title" VARCHAR(255) NOT NULL,
+ "author" VARCHAR(255),
+ "content" TEXT,
+ "link" VARCHAR(1023) NOT NULL,
+ "date" INT,
+ "lastSeen" INT DEFAULT 0,
+ "hash" BYTEA,
+ "is_read" SMALLINT NOT NULL DEFAULT 0,
+ "is_favorite" SMALLINT NOT NULL DEFAULT 0,
+ "id_feed" SMALLINT,
+ "tags" VARCHAR(1023),
+ FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ UNIQUE ("id_feed","guid")
+);',
+'CREATE INDEX %1$sis_favorite_index ON "%1$sentry" ("is_favorite");',
+'CREATE INDEX %1$sis_read_index ON "%1$sentry" ("is_read");',
+'CREATE INDEX %1$sentry_lastSeen_index ON "%1$sentry" ("lastSeen");',
+
+'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);',
+);
+
+global $SQL_INSERT_FEEDS;
+$SQL_INSERT_FEEDS = array(
+'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');',
+'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');',
+);
+
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentry", "%1$sfeed", "%1$scategory"');
diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php
index 87d5cf286..68d93ba92 100644
--- a/app/SQL/install.sql.sqlite.php
+++ b/app/SQL/install.sql.sqlite.php
@@ -1,16 +1,16 @@
<?php
global $SQL_CREATE_TABLES;
$SQL_CREATE_TABLES = array(
-'CREATE TABLE IF NOT EXISTS `%1$scategory` (
+'CREATE TABLE IF NOT EXISTS `category` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` varchar(255) NOT NULL,
UNIQUE (`name`)
);',
-'CREATE TABLE IF NOT EXISTS `%1$sfeed` (
+'CREATE TABLE IF NOT EXISTS `feed` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`url` varchar(511) NOT NULL,
- `%1$scategory` SMALLINT DEFAULT 0,
+ `category` SMALLINT DEFAULT 0,
`name` varchar(255) NOT NULL,
`website` varchar(255),
`description` text,
@@ -23,15 +23,15 @@ $SQL_CREATE_TABLES = array(
`ttl` INT NOT NULL DEFAULT -2,
`cache_nbEntries` int DEFAULT 0,
`cache_nbUnreads` int DEFAULT 0,
- FOREIGN KEY (`%1$scategory`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
+ FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
UNIQUE (`url`)
);',
-'CREATE INDEX IF NOT EXISTS feed_name_index ON `%1$sfeed`(`name`);',
-'CREATE INDEX IF NOT EXISTS feed_priority_index ON `%1$sfeed`(`priority`);',
-'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `%1$sfeed`(`keep_history`);',
+'CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);',
+'CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);',
+'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
-'CREATE TABLE IF NOT EXISTS `%1$sentry` (
+'CREATE TABLE IF NOT EXISTS `entry` (
`id` bigint NOT NULL,
`guid` varchar(760) NOT NULL,
`title` varchar(255) NOT NULL,
@@ -46,17 +46,21 @@ $SQL_CREATE_TABLES = array(
`id_feed` SMALLINT,
`tags` varchar(1023),
PRIMARY KEY (`id`),
- FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE (`id_feed`,`guid`)
);',
-'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `%1$sentry`(`is_favorite`);',
-'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);',
-'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);', //v1.1.1
+'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `entry`(`is_favorite`);',
+'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `entry`(`is_read`);',
+'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `entry`(`lastSeen`);', //v1.1.1
-'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");',
-'INSERT OR IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);',
-'INSERT OR IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);',
+'INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "%2$s");',
);
-define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
+global $SQL_INSERT_FEEDS;
+$SQL_INSERT_FEEDS = array(
+'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);',
+'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);',
+);
+
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS entry, feed, category');
diff --git a/app/actualize_script.php b/app/actualize_script.php
index fc4f9bfbb..deaa1bf7c 100755
--- a/app/actualize_script.php
+++ b/app/actualize_script.php
@@ -28,12 +28,13 @@ $app = new FreshRSS();
$system_conf = Minz_Configuration::get('system');
$system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!)
+FreshRSS_Context::$isCli = true;
// Create the list of users to actualize.
// Users are processed in a random order but always start with admin
$users = listUsers();
shuffle($users);
-if ($system_conf->default_user !== ''){
+if ($system_conf->default_user !== '') {
array_unshift($users, $system_conf->default_user);
$users = array_unique($users);
}
diff --git a/app/i18n/cz/admin.php b/app/i18n/cz/admin.php
index 881c02fc6..84bc58a17 100644
--- a/app/i18n/cz/admin.php
+++ b/app/i18n/cz/admin.php
@@ -33,7 +33,7 @@ return array(
'ok' => 'Máte požadovanou knihovnu pro ověřování znaků (ctype).',
),
'curl' => array(
- 'nok' => 'Nemáte cURL (balíček php5-curl).',
+ 'nok' => 'Nemáte cURL (balíček php-curl).',
'ok' => 'Máte rozšíření cURL.',
),
'data' => array(
@@ -71,8 +71,8 @@ return array(
'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
),
'pdo' => array(
- 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
- 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP instalace',
@@ -93,7 +93,7 @@ return array(
'ok' => 'Oprávnění adresáře users jsou v pořádku.',
),
'zip' => array(
- 'nok' => 'Nemáte rozšíření ZIP (balíček php5-zip).',
+ 'nok' => 'Nemáte rozšíření ZIP (balíček php-zip).',
'ok' => 'Máte rozšíření ZIP.',
),
),
diff --git a/app/i18n/cz/feedback.php b/app/i18n/cz/feedback.php
index 81302afca..f2bd87c77 100644
--- a/app/i18n/cz/feedback.php
+++ b/app/i18n/cz/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s neexistuje',
),
'import_export' => array(
- 'export_no_zip_extension' => 'Na serveru není naistalována podpora zip. Zkuste prosím exportovat soubory jeden po druhém.',
+ 'export_no_zip_extension' => 'Na serveru není naistalována podpora ZIP. Zkuste prosím exportovat soubory jeden po druhém.',
'feeds_imported' => 'Vaše kanály byly naimportovány a nyní budou aktualizovány',
'feeds_imported_with_errors' => 'Vaše kanály byly naimportovány, došlo ale k nějakým chybám',
'file_cannot_be_uploaded' => 'Soubor nelze nahrát!',
- 'no_zip_extension' => 'Na serveru není naistalována podpora zip.',
- 'zip_error' => 'Během importu zip souboru došlo k chybě.',
+ 'no_zip_extension' => 'Na serveru není naistalována podpora ZIP.',
+ 'zip_error' => 'Během importu ZIP souboru došlo k chybě.',
),
'sub' => array(
'actualize' => 'Aktualizovat',
diff --git a/app/i18n/cz/install.php b/app/i18n/cz/install.php
index 6b94c0d4b..c40ae46e2 100644
--- a/app/i18n/cz/install.php
+++ b/app/i18n/cz/install.php
@@ -41,7 +41,7 @@ return array(
'ok' => 'Je nainstalována požadovaná knihovna pro ověřování znaků (ctype).',
),
'curl' => array(
- 'nok' => 'Nemáte cURL (balíček php5-curl).',
+ 'nok' => 'Nemáte cURL (balíček php-curl).',
'ok' => 'Máte rozšíření cURL.',
),
'data' => array(
@@ -73,8 +73,8 @@ return array(
'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
),
'pdo' => array(
- 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
- 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.',
diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php
index cea0541e3..274cf16e1 100644
--- a/app/i18n/cz/sub.php
+++ b/app/i18n/cz/sub.php
@@ -44,8 +44,8 @@ return array(
'export_opml' => 'Exportovat seznam kanálů (OPML)',
'export_starred' => 'Exportovat oblíbené',
'feed_list' => 'Seznam %s článků',
- 'file_to_import' => 'Soubor k importu<br />(OPML, Json nebo Zip)',
- 'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo Json)',
+ 'file_to_import' => 'Soubor k importu<br />(OPML, JSON nebo ZIP)',
+ 'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo JSON)',
'import' => 'Import',
'starred_list' => 'Seznam oblíbených článků',
'title' => 'Import / export',
diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php
index 7b75fe5f4..c12f32bc8 100644
--- a/app/i18n/de/admin.php
+++ b/app/i18n/de/admin.php
@@ -33,7 +33,7 @@ return array(
'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
),
'curl' => array(
- 'nok' => 'Ihnen fehlt cURL (Paket php5-curl).',
+ 'nok' => 'Ihnen fehlt cURL (Paket php-curl).',
'ok' => 'Sie haben die cURL-Erweiterung.',
),
'data' => array(
@@ -71,8 +71,8 @@ return array(
'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
),
'pdo' => array(
- 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
- 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP-Installation',
@@ -93,7 +93,7 @@ return array(
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/users</em> sind in Ordnung.',
),
'zip' => array(
- 'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php5-zip).',
+ 'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php-zip).',
'ok' => 'Sie haben die ZIP-Erweiterung.',
),
),
diff --git a/app/i18n/de/feedback.php b/app/i18n/de/feedback.php
index f93992982..195083b36 100644
--- a/app/i18n/de/feedback.php
+++ b/app/i18n/de/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s existiert nicht',
),
'import_export' => array(
- 'export_no_zip_extension' => 'Die Zip-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie die 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' => '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.',
+ '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',
diff --git a/app/i18n/de/install.php b/app/i18n/de/install.php
index a77822e7b..178e8ea4c 100644
--- a/app/i18n/de/install.php
+++ b/app/i18n/de/install.php
@@ -4,7 +4,7 @@ return array(
'action' => array(
'finish' => 'Installation fertigstellen',
'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
- 'keep_install' => 'Vorherige Installation beibehalten (Daten)',
+ 'keep_install' => 'Vorherige Konfiguration beibehalten',
'next_step' => 'Zum nächsten Schritt springen',
'reinstall' => 'Neuinstallation von FreshRSS',
),
@@ -41,7 +41,7 @@ return array(
'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
),
'curl' => array(
- 'nok' => 'Ihnen fehlt cURL (Paket php5-curl).',
+ 'nok' => 'Ihnen fehlt cURL (Paket php-curl).',
'ok' => 'Sie haben die cURL-Erweiterung.',
),
'data' => array(
@@ -73,8 +73,8 @@ return array(
'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
),
'pdo' => array(
- 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
- 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.',
diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php
index 0f05a5635..cc98fd25c 100644
--- a/app/i18n/de/sub.php
+++ b/app/i18n/de/sub.php
@@ -44,7 +44,7 @@ return array(
'export_opml' => 'Liste der Feeds exportieren (OPML)',
'export_starred' => 'Ihre Favoriten exportieren',
'feed_list' => 'Liste von %s Artikeln',
- 'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder Zip)',
+ 'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder ZIP)',
'file_to_import_no_zip' => 'Zu importierende Datei<br />(OPML oder JSON)',
'import' => 'Importieren',
'starred_list' => 'Liste der Lieblingsartikel',
diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php
index a88552087..e5286d948 100644
--- a/app/i18n/en/admin.php
+++ b/app/i18n/en/admin.php
@@ -29,12 +29,12 @@ return array(
'ok' => 'Connection to the database is ok.',
),
'ctype' => array(
- 'nok' => 'You lack a required library for character type checking (php-ctype).',
+ 'nok' => 'Cannot find 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.',
+ 'nok' => 'Cannot find the cURL library (php-curl package).',
+ 'ok' => 'You have the cURL library.',
),
'data' => array(
'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
@@ -42,7 +42,7 @@ return array(
),
'database' => 'Database installation',
'dom' => array(
- 'nok' => 'You lack a required library to browse the DOM (php-xml package).',
+ 'nok' => 'Cannot find a required library to browse the DOM (php-xml package).',
'ok' => 'You have the required library to browse the DOM.',
),
'entries' => array(
@@ -59,20 +59,20 @@ return array(
),
'files' => 'File installation',
'json' => array(
- 'nok' => 'You lack JSON (php5-json package).',
+ 'nok' => 'Cannot find JSON (php5-json package).',
'ok' => 'You have JSON extension.',
),
'minz' => array(
- 'nok' => 'You lack the Minz framework.',
+ 'nok' => 'Cannot find the Minz framework.',
'ok' => 'You have the Minz framework.',
),
'pcre' => array(
- 'nok' => 'You lack a required library for regular expressions (php-pcre).',
+ 'nok' => 'Cannot find 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).',
+ 'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP installation',
@@ -93,7 +93,7 @@ return array(
'ok' => 'Permissions on users directory are good.',
),
'zip' => array(
- 'nok' => 'You lack ZIP extension (php5-zip package).',
+ 'nok' => 'Cannot find ZIP extension (php-zip package).',
'ok' => 'You have ZIP extension.',
),
),
diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php
index 7ce2ae9cf..e7f6b9f85 100644
--- a/app/i18n/en/feedback.php
+++ b/app/i18n/en/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s does not exist',
),
'import_export' => array(
- 'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.',
+ 'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
'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',
'file_cannot_be_uploaded' => 'File cannot be uploaded!',
- 'no_zip_extension' => 'Zip extension is not present on your server.',
- 'zip_error' => 'An error occured during Zip import.',
+ 'no_zip_extension' => 'ZIP extension is not present on your server.',
+ 'zip_error' => 'An error occured during ZIP import.',
),
'sub' => array(
'actualize' => 'Actualise',
diff --git a/app/i18n/en/install.php b/app/i18n/en/install.php
index d1c5f37c8..63d31fe53 100644
--- a/app/i18n/en/install.php
+++ b/app/i18n/en/install.php
@@ -4,7 +4,7 @@ return array(
'action' => array(
'finish' => 'Complete installation',
'fix_errors_before' => 'Please fix errors before skipping to the next step.',
- 'keep_install' => 'Keep previous installation',
+ 'keep_install' => 'Keep previous configuration',
'next_step' => 'Go to the next step',
'reinstall' => 'Reinstall FreshRSS',
),
@@ -25,9 +25,9 @@ return array(
),
'host' => 'Host',
'prefix' => 'Table prefix',
- 'password' => 'HTTP password',
+ 'password' => 'Database password',
'type' => 'Type of database',
- 'username' => 'HTTP username',
+ 'username' => 'Database username',
),
'check' => array(
'_' => 'Checks',
@@ -37,19 +37,19 @@ return array(
'ok' => 'Permissions on cache directory are good.',
),
'ctype' => array(
- 'nok' => 'You lack a required library for character type checking (php-ctype).',
+ 'nok' => 'Cannot find 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.',
+ 'nok' => 'Cannot find the cURL library (php-curl package).',
+ 'ok' => 'You have the cURL library.',
),
'data' => array(
'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on data directory are good.',
),
'dom' => array(
- 'nok' => 'You lack a required library to browse the DOM.',
+ 'nok' => 'Cannot find a required library to browse the DOM.',
'ok' => 'You have the required library to browse the DOM.',
),
'favicons' => array(
@@ -61,20 +61,20 @@ return array(
'ok' => 'Your HTTP REFERER is known and corresponds to your server.',
),
'json' => array(
- 'nok' => 'You lack a recommended library to parse JSON.',
+ 'nok' => 'Cannot find a recommended library to parse JSON.',
'ok' => 'You have a recommended library to parse JSON.',
),
'minz' => array(
- 'nok' => 'You lack the Minz framework.',
+ 'nok' => 'Cannot find the Minz framework.',
'ok' => 'You have the Minz framework.',
),
'pcre' => array(
- 'nok' => 'You lack a required library for regular expressions (php-pcre).',
+ 'nok' => 'Cannot find 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).',
+ 'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.',
@@ -85,7 +85,7 @@ return array(
'ok' => 'Permissions on users directory are good.',
),
'xml' => array(
- 'nok' => 'You lack the required library to parse XML.',
+ 'nok' => 'Cannot find the required library to parse XML.',
'ok' => 'You have the required library to parse XML.',
),
),
diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php
index aaaa02827..789433ee6 100644
--- a/app/i18n/en/sub.php
+++ b/app/i18n/en/sub.php
@@ -44,8 +44,8 @@ return array(
'export_opml' => 'Export list of feeds (OPML)',
'export_starred' => 'Export your favourites',
'feed_list' => 'List of %s articles',
- 'file_to_import' => 'File to import<br />(OPML, Json or Zip)',
- 'file_to_import_no_zip' => 'File to import<br />(OPML or Json)',
+ 'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)',
+ 'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)',
'import' => 'Import',
'starred_list' => 'List of favourite articles',
'title' => 'Import / export',
diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php
index c359e9d24..e9263a5e2 100644
--- a/app/i18n/fr/admin.php
+++ b/app/i18n/fr/admin.php
@@ -29,12 +29,12 @@ return array(
'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).',
+ 'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).',
+ 'ok' => 'Vous disposez de la librairie 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.',
+ 'nok' => 'Impossible de trouver la librairie cURL (paquet php-curl).',
+ 'ok' => 'Vous disposez de la librairie cURL.',
),
'data' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data</em>. Le serveur HTTP doit être capable d’écrire dedans',
@@ -42,8 +42,8 @@ return array(
),
'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.',
+ 'nok' => 'Impossible de trouver une librairie pour parcourir le DOM (paquet php-xml).',
+ 'ok' => 'Vous disposez de la librairie pour parcourir le DOM.',
),
'entries' => array(
'nok' => 'La table entry est mal configurée.',
@@ -60,19 +60,19 @@ return array(
'files' => 'Installation des fichiers',
'json' => array(
'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).',
- 'ok' => 'Vous disposez de l\'extension JSON.',
+ 'ok' => 'Vous disposez de l’extension JSON.',
),
'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).',
+ 'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).',
+ 'ok' => 'Vous disposez de la librairie 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).',
+ 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'Installation de PHP',
@@ -80,7 +80,7 @@ return array(
'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.',
+ 'nok' => 'Impossible de trouver une ou plusieurs tables en base de données.',
'ok' => 'Les tables sont bien présentes en base de données.',
),
'title' => 'Vérification de l’installation',
@@ -93,8 +93,8 @@ return array(
'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
),
'zip' => array(
- 'nok' => 'Vous ne disposez pas de l\'extension ZIP (paquet php5-zip).',
- 'ok' => 'Vous disposez de l\'extension ZIP.',
+ 'nok' => 'Vous ne disposez pas de l’extension ZIP (paquet php-zip).',
+ 'ok' => 'Vous disposez de l’extension ZIP.',
),
),
'extensions' => array(
diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php
index 15f3ab859..5966fc3a7 100644
--- a/app/i18n/fr/feedback.php
+++ b/app/i18n/fr/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s n’existe pas',
),
'import_export' => array(
- '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_no_zip_extension' => 'L’extension ZIP n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.',
'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.',
'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé !',
- 'no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur.',
- 'zip_error' => 'Une erreur est survenue durant l’import du fichier Zip.',
+ 'no_zip_extension' => 'L’extension ZIP n’est pas présente sur votre serveur.',
+ 'zip_error' => 'Une erreur est survenue durant l’import du fichier ZIP.',
),
'sub' => array(
'actualize' => 'Actualiser',
diff --git a/app/i18n/fr/install.php b/app/i18n/fr/install.php
index 946a210ee..c50807107 100644
--- a/app/i18n/fr/install.php
+++ b/app/i18n/fr/install.php
@@ -24,10 +24,10 @@ return array(
'ok' => 'La configuration de la base de données a été enregistrée.',
),
'host' => 'Hôte',
- 'password' => 'Mot de passe',
+ 'password' => 'Mot de passe pour base de données',
'prefix' => 'Préfixe des tables',
'type' => 'Type de base de données',
- 'username' => 'Nom d’utilisateur',
+ 'username' => 'Nom d’utilisateur pour base de données',
),
'check' => array(
'_' => 'Vérifications',
@@ -37,11 +37,11 @@ return array(
'ok' => 'Les droits sur le répertoire de cache sont bons.',
),
'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).',
+ 'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).',
+ 'ok' => 'Vous disposez de la librairie pour la vérification des types de caractères (ctype).',
),
'curl' => array(
- 'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).',
+ 'nok' => 'Vous ne disposez pas de cURL (paquet php-curl).',
'ok' => 'Vous disposez de cURL.',
),
'data' => array(
@@ -49,8 +49,8 @@ return array(
'ok' => 'Les droits sur le répertoire de data sont bons.',
),
'dom' => array(
- 'nok' => 'Il manque une librairie pour parcourir le DOM.',
- 'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.',
+ 'nok' => 'Impossible de trouver une librairie pour parcourir le DOM.',
+ 'ok' => 'Vous disposez de la librairie pour parcourir le DOM.',
),
'favicons' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/favicons</em>. Le serveur HTTP doit être capable d’écrire dedans',
@@ -61,7 +61,7 @@ return array(
'ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.',
),
'json' => array(
- 'nok' => 'Il manque une librairie recommandée pour JSON.',
+ 'nok' => 'Impossible de trouver une librairie recommandée pour JSON.',
'ok' => 'Vouz disposez de la librairie recommandée pour JSON.',
),
'minz' => array(
@@ -69,12 +69,12 @@ return array(
'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).',
+ 'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).',
+ 'ok' => 'Vous disposez de la librairie 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).',
+ 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.',
@@ -85,7 +85,7 @@ return array(
'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
),
'xml' => array(
- 'nok' => 'Il manque une librairie requise pour XML.',
+ 'nok' => 'Impossible de trouver une librairie requise pour XML.',
'ok' => 'Vouz disposez de la librairie requise pour XML.',
),
),
diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php
index e3631eb8b..bb3f2fefc 100644
--- a/app/i18n/fr/sub.php
+++ b/app/i18n/fr/sub.php
@@ -44,8 +44,8 @@ return array(
'export_opml' => 'Exporter la liste des flux (OPML)',
'export_starred' => 'Exporter les favoris',
'feed_list' => 'Liste des articles de %s',
- 'file_to_import' => 'Fichier à importer<br />(OPML, Json ou Zip)',
- 'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou Json)',
+ 'file_to_import' => 'Fichier à importer<br />(OPML, JSON ou ZIP)',
+ 'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou JSON)',
'import' => 'Importer',
'starred_list' => 'Liste des articles favoris',
'title' => 'Importer / exporter',
diff --git a/app/i18n/it/admin.php b/app/i18n/it/admin.php
index 4eea158f6..c399bc8c8 100644
--- a/app/i18n/it/admin.php
+++ b/app/i18n/it/admin.php
@@ -33,7 +33,7 @@ return array(
'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).',
),
'curl' => array(
- 'nok' => 'Manca il supporto per cURL (pacchetto php5-curl).',
+ 'nok' => 'Manca il supporto per cURL (pacchetto php-curl).',
'ok' => 'Estensione cURL presente.',
),
'data' => array(
@@ -71,8 +71,8 @@ return array(
'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
),
'pdo' => array(
- 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).',
- 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'Installazione PHP',
@@ -93,7 +93,7 @@ return array(
'ok' => 'I permessi sulla cartella users sono corretti.',
),
'zip' => array(
- 'nok' => 'Manca estensione ZIP (pacchetto php5-zip).',
+ 'nok' => 'Manca estensione ZIP (pacchetto php-zip).',
'ok' => 'Estensione ZIP presente.',
),
),
diff --git a/app/i18n/it/feedback.php b/app/i18n/it/feedback.php
index f217586b0..5851cb2e6 100644
--- a/app/i18n/it/feedback.php
+++ b/app/i18n/it/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s non disponibile',
),
'import_export' => array(
- 'export_no_zip_extension' => 'Estensione Zip non presente sul server. Per favore esporta i files singolarmente.',
+ 'export_no_zip_extension' => 'Estensione ZIP non presente sul server. Per favore esporta i files singolarmente.',
'feeds_imported' => 'I tuoi feed sono stati importati e saranno aggiornati',
'feeds_imported_with_errors' => 'I tuoi feeds sono stati importati ma si sono verificati alcuni errori',
'file_cannot_be_uploaded' => 'Il file non può essere caricato!',
- 'no_zip_extension' => 'Estensione Zip non presente sul server.',
- 'zip_error' => 'Si è verificato un errore importando il file Zip',
+ 'no_zip_extension' => 'Estensione ZIP non presente sul server.',
+ 'zip_error' => 'Si è verificato un errore importando il file ZIP',
),
'sub' => array(
'actualize' => 'Aggiorna',
diff --git a/app/i18n/it/install.php b/app/i18n/it/install.php
index a60dd4523..2fa298508 100644
--- a/app/i18n/it/install.php
+++ b/app/i18n/it/install.php
@@ -4,7 +4,7 @@ return array(
'action' => array(
'finish' => 'Installazione completata',
'fix_errors_before' => 'Per favore correggi gli errori prima di passare al passaggio successivo.',
- 'keep_install' => 'Mantieni installazione precedente',
+ 'keep_install' => 'Mantieni configurazione precedente',
'next_step' => 'Vai al prossimo passaggio',
'reinstall' => 'Reinstalla FreshRSS',
),
@@ -25,9 +25,9 @@ return array(
),
'host' => 'Host',
'prefix' => 'Prefisso tabella',
- 'password' => 'HTTP password',
+ 'password' => 'Password del database',
'type' => 'Tipo di database',
- 'username' => 'HTTP username',
+ 'username' => 'Nome utente del database',
),
'check' => array(
'_' => 'Controlli',
@@ -41,7 +41,7 @@ return array(
'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).',
),
'curl' => array(
- 'nok' => 'Manca il supporto per cURL (pacchetto php5-curl).',
+ 'nok' => 'Manca il supporto per cURL (pacchetto php-curl).',
'ok' => 'Estensione cURL presente.',
),
'data' => array(
@@ -73,8 +73,8 @@ return array(
'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
),
'pdo' => array(
- 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).',
- 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'Installazione PHP',
diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php
index dfcee2ce3..758e322c5 100644
--- a/app/i18n/it/sub.php
+++ b/app/i18n/it/sub.php
@@ -44,8 +44,8 @@ return array(
'export_opml' => 'Esporta tutta la lista dei feed (OPML)',
'export_starred' => 'Esporta i tuoi preferiti',
'feed_list' => 'Elenco di %s articoli',
- 'file_to_import' => 'File da importare<br />(OPML, Json o Zip)',
- 'file_to_import_no_zip' => 'File da importare<br />(OPML o Json)',
+ 'file_to_import' => 'File da importare<br />(OPML, JSON o ZIP)',
+ 'file_to_import_no_zip' => 'File da importare<br />(OPML o JSON)',
'import' => 'Importa',
'starred_list' => 'Elenco articoli preferiti',
'title' => 'Importa / esporta',
diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php
index 9f05d69b1..59637bfef 100644
--- a/app/i18n/nl/admin.php
+++ b/app/i18n/nl/admin.php
@@ -33,7 +33,7 @@ return array(
'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).',
),
'curl' => array(
- 'nok' => 'U mist de cURL (php5-curl package).',
+ 'nok' => 'U mist de cURL (php-curl package).',
'ok' => 'U hebt de cURL uitbreiding.',
),
'data' => array(
@@ -71,8 +71,8 @@ return array(
'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
),
'pdo' => array(
- 'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
- 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP installatie',
@@ -93,7 +93,7 @@ return array(
'ok' => 'Permissies op de users map zijn goed.',
),
'zip' => array(
- 'nok' => 'U mist ZIP uitbreiding (php5-zip package).',
+ 'nok' => 'U mist ZIP uitbreiding (php-zip package).',
'ok' => 'U hebt ZIP uitbreiding.',
),
),
diff --git a/app/i18n/nl/feedback.php b/app/i18n/nl/feedback.php
index b703c43cf..386b8d415 100644
--- a/app/i18n/nl/feedback.php
+++ b/app/i18n/nl/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s bestaat niet',
),
'import_export' => array(
- 'export_no_zip_extension' => 'Zip uitbreiding is niet aanwezig op uw server. Exporteer a.u.b. uw bestanden één voor één.',
+ 'export_no_zip_extension' => 'ZIP uitbreiding is niet aanwezig op uw server. Exporteer a.u.b. uw bestanden één voor één.',
'feeds_imported' => 'Uw feeds zijn geimporteerd en worden nu vernieuwd',
'feeds_imported_with_errors' => 'Uw feeds zijn geimporteerd maar er zijn enige fouten opgetreden',
'file_cannot_be_uploaded' => 'Bestand kan niet worden verzonden!',
- 'no_zip_extension' => 'Zip uitbreiding is niet aanwezig op uw server.',
- 'zip_error' => 'Er is een fout opgetreden tijdens het imporeren van het Zip bestand.',
+ 'no_zip_extension' => 'ZIP uitbreiding is niet aanwezig op uw server.',
+ 'zip_error' => 'Er is een fout opgetreden tijdens het imporeren van het ZIP bestand.',
),
'sub' => array(
'actualize' => 'Actualiseren',
diff --git a/app/i18n/nl/install.php b/app/i18n/nl/install.php
index 77783cd48..bd2ba0cce 100644
--- a/app/i18n/nl/install.php
+++ b/app/i18n/nl/install.php
@@ -25,9 +25,9 @@ return array(
),
'host' => 'Host',
'prefix' => 'Tabel voorvoegsel',
- 'password' => 'HTTP wachtwoord',
+ 'password' => 'Database wachtwoord',
'type' => 'Type database',
- 'username' => 'HTTP gebruikersnaam',
+ 'username' => 'Database gebruikersnaam',
),
'check' => array(
'_' => 'Controles',
@@ -41,7 +41,7 @@ return array(
'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).',
),
'curl' => array(
- 'nok' => 'U mist cURL (php5-curl package).',
+ 'nok' => 'U mist cURL (php-curl package).',
'ok' => 'U hebt de cURL uitbreiding.',
),
'data' => array(
@@ -73,8 +73,8 @@ return array(
'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
),
'pdo' => array(
- 'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite).',
- 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Uw PHP versie is %s maar FreshRSS benodigd tenminste versie %s.',
diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php
index 159a58b27..a1ba3f03d 100644
--- a/app/i18n/nl/sub.php
+++ b/app/i18n/nl/sub.php
@@ -44,8 +44,8 @@ return array(
'export_opml' => 'Exporteer lijst van feeds (OPML)',
'export_starred' => 'Exporteer je fovorieten',
'feed_list' => 'Lijst van %s artikelen',
- 'file_to_import' => 'Bestand om te importeren<br />(OPML, Json of Zip)',
- 'file_to_import_no_zip' => 'Bestand om te importeren<br />(OPML of Json)',
+ 'file_to_import' => 'Bestand om te importeren<br />(OPML, JSON of ZIP)',
+ 'file_to_import_no_zip' => 'Bestand om te importeren<br />(OPML of JSON)',
'import' => 'Importeer',
'starred_list' => 'Lijst van favoriete artikelen',
'title' => 'Importeren / exporteren',
diff --git a/app/i18n/ru/admin.php b/app/i18n/ru/admin.php
index caea627f3..355100689 100644
--- a/app/i18n/ru/admin.php
+++ b/app/i18n/ru/admin.php
@@ -33,7 +33,7 @@ return array(
'ok' => 'У вас не установлена библиотека для проверки типов символов (ctype).',
),
'curl' => array(
- 'nok' => 'У вас не установлено расширение cURL (пакет php5-curl).',
+ 'nok' => 'У вас не установлено расширение cURL (пакет php-curl).',
'ok' => 'У вас установлено расширение cURL.',
),
'data' => array(
@@ -71,8 +71,8 @@ return array(
'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
),
'pdo' => array(
- 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).',
- 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP installation',
@@ -93,7 +93,7 @@ return array(
'ok' => 'Права на папку users в порядке.',
),
'zip' => array(
- 'nok' => 'You lack ZIP extension (php5-zip package).',
+ 'nok' => 'You lack ZIP extension (php-zip package).',
'ok' => 'You have ZIP extension.',
),
),
diff --git a/app/i18n/ru/feedback.php b/app/i18n/ru/feedback.php
index 7ce2ae9cf..e7f6b9f85 100644
--- a/app/i18n/ru/feedback.php
+++ b/app/i18n/ru/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s does not exist',
),
'import_export' => array(
- 'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.',
+ 'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
'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',
'file_cannot_be_uploaded' => 'File cannot be uploaded!',
- 'no_zip_extension' => 'Zip extension is not present on your server.',
- 'zip_error' => 'An error occured during Zip import.',
+ 'no_zip_extension' => 'ZIP extension is not present on your server.',
+ 'zip_error' => 'An error occured during ZIP import.',
),
'sub' => array(
'actualize' => 'Actualise',
diff --git a/app/i18n/ru/install.php b/app/i18n/ru/install.php
index a52e2959b..bad59bbb3 100644
--- a/app/i18n/ru/install.php
+++ b/app/i18n/ru/install.php
@@ -25,9 +25,9 @@ return array(
),
'host' => 'Хост',
'prefix' => 'Префикс таблицы',
- 'password' => 'Пароль HTTP',
+ 'password' => 'Пароль базы данных',
'type' => 'Тип базы данных',
- 'username' => 'Имя пользователя HTTP',
+ 'username' => 'Имя пользователя базы данных',
),
'check' => array(
'_' => 'Проверки',
@@ -41,7 +41,7 @@ return array(
'ok' => 'У вас установлена необходимая библиотека для проверки типов символов (ctype).',
),
'curl' => array(
- 'nok' => 'У вас нет расширения cURL (пакет php5-curl).',
+ 'nok' => 'У вас нет расширения cURL (пакет php-curl).',
'ok' => 'У вас установлено расширение cURL.',
),
'data' => array(
@@ -69,8 +69,8 @@ return array(
'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
),
'pdo' => array(
- 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).',
- 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'У вас установлен PHP версии %s, но FreshRSS необходима версия не ниже %s.',
diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php
index aaaa02827..789433ee6 100644
--- a/app/i18n/ru/sub.php
+++ b/app/i18n/ru/sub.php
@@ -44,8 +44,8 @@ return array(
'export_opml' => 'Export list of feeds (OPML)',
'export_starred' => 'Export your favourites',
'feed_list' => 'List of %s articles',
- 'file_to_import' => 'File to import<br />(OPML, Json or Zip)',
- 'file_to_import_no_zip' => 'File to import<br />(OPML or Json)',
+ 'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)',
+ 'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)',
'import' => 'Import',
'starred_list' => 'List of favourite articles',
'title' => 'Import / export',
diff --git a/app/i18n/tr/admin.php b/app/i18n/tr/admin.php
index 43f8e23c5..e0dbd288d 100644
--- a/app/i18n/tr/admin.php
+++ b/app/i18n/tr/admin.php
@@ -33,7 +33,7 @@ return array(
'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).',
),
'curl' => array(
- 'nok' => 'cURL eksik (php5-curl package).',
+ 'nok' => 'cURL eksik (php-curl package).',
'ok' => 'cURL eklentisi sorunsuz.',
),
'data' => array(
@@ -71,8 +71,8 @@ return array(
'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
),
'pdo' => array(
- 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).',
- 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP kurulumu',
@@ -93,7 +93,7 @@ return array(
'ok' => 'Kullanıcılar klasörü yetkileri sorunsuz.',
),
'zip' => array(
- 'nok' => 'ZIP eklentisi eksik (php5-zip package).',
+ 'nok' => 'ZIP eklentisi eksik (php-zip package).',
'ok' => 'ZIP eklentisi sorunsuz.',
),
),
diff --git a/app/i18n/tr/feedback.php b/app/i18n/tr/feedback.php
index a53316206..87361ff51 100644
--- a/app/i18n/tr/feedback.php
+++ b/app/i18n/tr/feedback.php
@@ -43,12 +43,12 @@ return array(
'not_found' => '%s bulunmamaktadır',
),
'import_export' => array(
- 'export_no_zip_extension' => 'Zip eklentisi mevcut sunucunuzda yer almıyor. Lütfen başka dosya formatında dışarı aktarmayı deneyin.',
+ 'export_no_zip_extension' => 'ZIP eklentisi mevcut sunucunuzda yer almıyor. Lütfen başka dosya formatında dışarı aktarmayı deneyin.',
'feeds_imported' => 'Akışlarınız içe aktarıldı ve şimdi güncellenecek',
'feeds_imported_with_errors' => 'Akışlarınız içeri aktarıldı ama bazı hatalar meydana geldi',
'file_cannot_be_uploaded' => 'Dosya yüklenemedi!',
- 'no_zip_extension' => 'Zip eklentisi mevcut sunucunuzda yer almıyor.',
- 'zip_error' => 'Zip içe aktarımı sırasında hata meydana geldi.',
+ 'no_zip_extension' => 'ZIP eklentisi mevcut sunucunuzda yer almıyor.',
+ 'zip_error' => 'ZIP içe aktarımı sırasında hata meydana geldi.',
),
'sub' => array(
'actualize' => 'Güncelleme',
diff --git a/app/i18n/tr/install.php b/app/i18n/tr/install.php
index 951a7c5fd..4daa45099 100644
--- a/app/i18n/tr/install.php
+++ b/app/i18n/tr/install.php
@@ -25,9 +25,9 @@ return array(
),
'host' => 'Sunucu',
'prefix' => 'Tablo ön eki',
- 'password' => 'HTTP şifre',
+ 'password' => 'Veritabanı şifresi',
'type' => 'Veritabanı türü',
- 'username' => 'HTTP kullanıcı adı',
+ 'username' => 'Veritabanı kullanıcı adı',
),
'check' => array(
'_' => 'Kontroller',
@@ -41,7 +41,7 @@ return array(
'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).',
),
'curl' => array(
- 'nok' => 'cURL eksik (php5-curl package).',
+ 'nok' => 'cURL eksik (php-curl package).',
'ok' => 'cURL eklentisi sorunsuz.',
),
'data' => array(
@@ -73,8 +73,8 @@ return array(
'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
),
'pdo' => array(
- 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).',
- 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).',
+ 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+ 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'PHP versiyonunuz %s fakat FreshRSS için gerekli olan en düşük sürüm %s.',
diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php
index 5ab367ebb..7592096d9 100644
--- a/app/i18n/tr/sub.php
+++ b/app/i18n/tr/sub.php
@@ -44,8 +44,8 @@ return array(
'export_opml' => 'Akış listesini dışarı aktar (OPML)',
'export_starred' => 'Favorileri dışarı aktar',
'feed_list' => '%s makalenin listesi',
- 'file_to_import' => 'Dosyadan içe aktar<br />(OPML, Json or Zip)',
- 'file_to_import_no_zip' => 'Dosyadan içe aktar<br />(OPML or Json)',
+ 'file_to_import' => 'Dosyadan içe aktar<br />(OPML, JSON or ZIP)',
+ 'file_to_import_no_zip' => 'Dosyadan içe aktar<br />(OPML or JSON)',
'import' => 'İçe aktar',
'starred_list' => 'Favori makaleleirn listesi',
'title' => 'İçe / dışa aktar',
diff --git a/app/install.php b/app/install.php
index 62695ceb6..fcc901713 100644
--- a/app/install.php
+++ b/app/install.php
@@ -4,23 +4,18 @@ if (function_exists('opcache_reset')) {
}
header("Content-Security-Policy: default-src 'self'");
-define('BCRYPT_COST', 9);
+require(LIB_PATH . '/lib_install.php');
session_name('FreshRSS');
session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
session_start();
-Minz_Configuration::register('default_system', join_path(DATA_PATH, 'config.default.php'));
-Minz_Configuration::register('default_user', join_path(USERS_PATH, '_', 'config.default.php'));
-
if (isset($_GET['step'])) {
define('STEP',(int)$_GET['step']);
} else {
define('STEP', 0);
}
-define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
-
if (STEP === 3 && isset($_POST['type'])) {
$_SESSION['bd_type'] = $_POST['type'];
}
@@ -28,10 +23,13 @@ if (STEP === 3 && isset($_POST['type'])) {
if (isset($_SESSION['bd_type'])) {
switch ($_SESSION['bd_type']) {
case 'mysql':
- include(APP_PATH . '/SQL/install.sql.mysql.php');
+ include_once(APP_PATH . '/SQL/install.sql.mysql.php');
break;
case 'sqlite':
- include(APP_PATH . '/SQL/install.sql.sqlite.php');
+ include_once(APP_PATH . '/SQL/install.sql.sqlite.php');
+ break;
+ case 'pgsql':
+ include_once(APP_PATH . '/SQL/install.sql.pgsql.php');
break;
}
}
@@ -130,12 +128,7 @@ function saveStep2() {
$password_plain = param('passwordPlain', false);
if ($password_plain !== false && cryptAvailable()) {
- if (!function_exists('password_hash')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- $passwordHash = password_hash($password_plain, PASSWORD_BCRYPT, array('cost' => BCRYPT_COST));
- $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
- $_SESSION['passwordHash'] = $passwordHash;
+ $_SESSION['passwordHash'] = FreshRSS_user_Controller::hashPassword($password_plain);
}
if (empty($_SESSION['old_entries']) ||
@@ -148,7 +141,7 @@ function saveStep2() {
return false;
}
- $_SESSION['salt'] = sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
+ $_SESSION['salt'] = generateSalt();
if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
$_SESSION['old_entries'] = $user_default_config->old_entries;
}
@@ -170,7 +163,7 @@ function saveStep2() {
recursive_unlink($user_dir);
mkdir($user_dir);
- file_put_contents($user_config_path, "<?php\n return " . var_export($config_array, true) . ';');
+ file_put_contents($user_config_path, "<?php\n return " . var_export($config_array, true) . ";\n");
header('Location: index.php?step=3');
}
@@ -199,6 +192,9 @@ function saveStep3() {
$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_'));
}
+ if ($_SESSION['bd_type'] === 'pgsql') {
+ $_SESSION['bd_base'] = strtolower($_SESSION['bd_base']);
+ }
// We use dirname to remove the /i part
$base_url = dirname(Minz_Request::guessBaseUrl());
@@ -221,55 +217,30 @@ function saveStep3() {
);
@unlink(join_path(DATA_PATH, 'config.php')); //To avoid access-rights problems
- file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config_array, true) . ';');
+ file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config_array, true) . ";\n");
- $res = checkBD();
+ $config_array['db']['default_user'] = $config_array['default_user'];
+ $config_array['db']['prefix_user'] = $_SESSION['bd_prefix_user'];
+ $ok = checkDb($config_array['db']) && checkDbUser($config_array['db']);
+ if (!$ok) {
+ @unlink(join_path(DATA_PATH, 'config.php'));
+ }
- if ($res) {
+ if ($ok) {
$_SESSION['bd_error'] = '';
header('Location: index.php?step=4');
- } elseif (empty($_SESSION['bd_error'])) {
- $_SESSION['bd_error'] = 'Unknown error!';
+ } else {
+ $_SESSION['bd_error'] = empty($config_array['db']['bd_error']) ? 'Unknown error!' : $config_array['db']['bd_error'];
}
}
invalidateHttpCache();
}
-function newPdo() {
- switch ($_SESSION['bd_type']) {
- case 'mysql':
- $str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
- $driver_options = array(
- PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
- );
- break;
- case 'sqlite':
- $str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite');
- $driver_options = array(
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- );
- break;
- default:
- return false;
- }
- return new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
-}
-
-function deleteInstall() {
- $res = unlink(join_path(DATA_PATH, 'do-install.txt'));
-
- if (!$res) {
- return false;
- }
-
- header('Location: index.php');
-}
-
/*** VÉRIFICATIONS ***/
function checkStep() {
$s0 = checkStep0();
- $s1 = checkStep1();
+ $s1 = checkRequirements();
$s2 = checkStep2();
$s3 = checkStep3();
if (STEP > 0 && $s0['all'] != 'ok') {
@@ -295,47 +266,6 @@ function checkStep0() {
);
}
-function checkStep1() {
- $php = version_compare(PHP_VERSION, '5.3.3') >= 0;
- $minz = file_exists(join_path(LIB_PATH, 'Minz'));
- $curl = extension_loaded('curl');
- $pdo_mysql = extension_loaded('pdo_mysql');
- $pdo_sqlite = extension_loaded('pdo_sqlite');
- $pdo = $pdo_mysql || $pdo_sqlite;
- $pcre = extension_loaded('pcre');
- $ctype = extension_loaded('ctype');
- $dom = class_exists('DOMDocument');
- $xml = function_exists('xml_parser_create');
- $json = function_exists('json_encode');
- $data = DATA_PATH && is_writable(DATA_PATH);
- $cache = CACHE_PATH && is_writable(CACHE_PATH);
- $users = USERS_PATH && is_writable(USERS_PATH);
- $favicons = is_writable(join_path(DATA_PATH, 'favicons'));
- $http_referer = is_referer_from_same_domain();
-
- return array(
- 'php' => $php ? 'ok' : 'ko',
- 'minz' => $minz ? 'ok' : 'ko',
- 'curl' => $curl ? 'ok' : 'ko',
- 'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
- 'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
- 'pdo' => $pdo ? 'ok' : 'ko',
- 'pcre' => $pcre ? 'ok' : 'ko',
- 'ctype' => $ctype ? 'ok' : 'ko',
- 'dom' => $dom ? 'ok' : 'ko',
- 'xml' => $xml ? 'ok' : 'ko',
- 'json' => $json ? 'ok' : 'ko',
- 'data' => $data ? 'ok' : 'ko',
- 'cache' => $cache ? 'ok' : 'ko',
- 'users' => $users ? 'ok' : 'ko',
- 'favicons' => $favicons ? 'ok' : 'ko',
- 'http_referer' => $http_referer ? 'ok' : 'ko',
- 'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $xml &&
- $data && $cache && $users && $favicons && $http_referer ?
- 'ok' : 'ko'
- );
-}
-
function freshrss_already_installed() {
$conf_path = join_path(DATA_PATH, 'config.php');
if (!file_exists($conf_path)) {
@@ -406,43 +336,15 @@ function checkStep3() {
);
}
-function checkBD() {
+function checkDbUser(&$dbOptions) {
$ok = false;
-
+ $str = $dbOptions['dsn'];
+ $driver_options = $dbOptions['options'];
try {
- $str = '';
- $driver_options = null;
- switch ($_SESSION['bd_type']) {
- case 'mysql':
- $driver_options = array(
- PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'
- );
-
- try { // on ouvre une connexion juste pour créer la base si elle n'existe pas
- $str = 'mysql:host=' . $_SESSION['bd_host'] . ';';
- $c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
- $sql = sprintf(SQL_CREATE_DB, $_SESSION['bd_base']);
- $res = $c->query($sql);
- } catch (PDOException $e) {
- }
-
- // on écrase la précédente connexion en sélectionnant la nouvelle BDD
- $str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
- break;
- case 'sqlite':
- $str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite');
- $driver_options = array(
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- );
- break;
- default:
- return false;
- }
-
- $c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
+ $c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options);
if (defined('SQL_CREATE_TABLES')) {
- $sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user'], _t('gen.short.default_category'));
+ $sql = sprintf(SQL_CREATE_TABLES, $dbOptions['prefix_user'], _t('gen.short.default_category'));
$stm = $c->prepare($sql);
$ok = $stm->execute();
} else {
@@ -450,7 +352,22 @@ function checkBD() {
if (is_array($SQL_CREATE_TABLES)) {
$ok = true;
foreach ($SQL_CREATE_TABLES as $instruction) {
- $sql = sprintf($instruction, $_SESSION['bd_prefix_user'], _t('gen.short.default_category'));
+ $sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));
+ $stm = $c->prepare($sql);
+ $ok &= $stm->execute();
+ }
+ }
+ }
+
+ if (defined('SQL_INSERT_FEEDS')) {
+ $sql = sprintf(SQL_INSERT_FEEDS, $dbOptions['prefix_user']);
+ $stm = $c->prepare($sql);
+ $ok &= $stm->execute();
+ } else {
+ global $SQL_INSERT_FEEDS;
+ if (is_array($SQL_INSERT_FEEDS)) {
+ foreach ($SQL_INSERT_FEEDS as $instruction) {
+ $sql = sprintf($instruction, $dbOptions['prefix_user']);
$stm = $c->prepare($sql);
$ok &= $stm->execute();
}
@@ -458,13 +375,8 @@ function checkBD() {
}
} catch (PDOException $e) {
$ok = false;
- $_SESSION['bd_error'] = $e->getMessage();
+ $dbOptions['bd_error'] = $e->getMessage();
}
-
- if (!$ok) {
- @unlink(join_path(DATA_PATH, 'config.php'));
- }
-
return $ok;
}
@@ -507,7 +419,7 @@ function printStep0() {
// @todo refactor this view with the check_install action
function printStep1() {
- $res = checkStep1();
+ $res = checkRequirements();
?>
<noscript><p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.attention'); ?></span> <?php echo _t('install.javascript_is_better'); ?></p></noscript>
@@ -690,7 +602,7 @@ function printStep3() {
<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.bdd.conf.ko'),(empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']); ?></p>
<?php } ?>
- <form action="index.php?step=3" method="post">
+ <form action="index.php?step=3" method="post" autocomplete="off">
<legend><?php echo _t('install.bdd.conf'); ?></legend>
<div class="form-group">
<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
@@ -708,6 +620,12 @@ function printStep3() {
SQLite
</option>
<?php }?>
+ <?php if (extension_loaded('pdo_pgsql')) {?>
+ <option value="pgsql"
+ <?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
+ PostgreSQL (⚠️ experimental)
+ </option>
+ <?php }?>
</select>
</div>
</div>
@@ -716,7 +634,7 @@ function printStep3() {
<div class="form-group">
<label class="group-name" for="host"><?php echo _t('install.bdd.host'); ?></label>
<div class="group-controls">
- <input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" />
+ <input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}(:[0-9]{2,5})?" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" />
</div>
</div>
@@ -730,7 +648,7 @@ function printStep3() {
<div class="form-group">
<label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label>
<div class="group-controls">
- <input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" />
+ <input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" autocomplete="off" />
</div>
</div>
@@ -796,7 +714,9 @@ case 3:
case 4:
break;
case 5:
- deleteInstall();
+ if (deleteInstall()) {
+ header('Location: index.php');
+ }
break;
}
?>
diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml
index e8fdbf842..3e1ee44dd 100644
--- a/app/layout/aside_feed.phtml
+++ b/app/layout/aside_feed.phtml
@@ -79,13 +79,13 @@
<?php if (FreshRSS_Auth::hasAccess()) { ?>
<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '------'); ?>"><?php echo _t('index.menu.stats'); ?></a></li>
<?php } ?>
- <li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li>
+ <li class="item"><a target="_blank" rel="noreferrer" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li>
<?php if (FreshRSS_Auth::hasAccess()) { ?>
<li class="separator"></li>
<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '------'); ?>"><?php echo _t('gen.action.manage'); ?></a></li>
<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '------'); ?>"><?php echo _t('gen.action.actualize'); ?></a></li>
<li class="item">
- <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>
+ <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>
<button class="read_all as-link <?php echo $confirm; ?>"
form="mark-read-aside"
formaction="<?php echo _url('entry', 'read', 'get', 'f_------'); ?>"
diff --git a/app/layout/aside_subscription.phtml b/app/layout/aside_subscription.phtml
index fa10d63e8..e14afe2a7 100644
--- a/app/layout/aside_subscription.phtml
+++ b/app/layout/aside_subscription.phtml
@@ -10,7 +10,7 @@
</li>
<li class="item">
- <a class="bookmarkClick" href="javascript:(function(){var%20url%20=%20location.href;window.open('<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&amp;url_rss='+encodeURIComponent(url), '_blank');})();">
+ <a class="bookmarkClick" href="javascript:(function(){var%20url%20=%20location.href;var%20otherWindow=window.open('about:blank','_blank');otherWindow.opener=null;otherWindow.location='<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&amp;url_rss='+encodeURIComponent(url);})();">
<?php echo _t('sub.menu.bookmark'); ?>
</a>
</li>
diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml
index 4d9ad5458..1f11e0af1 100644
--- a/app/layout/layout.phtml
+++ b/app/layout/layout.phtml
@@ -42,6 +42,9 @@
} if (isset($this->rss_title)) {
$url_rss = $url_base;
$url_rss['a'] = 'rss';
+ if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
+ $url_rss['params']['hours'] = FreshRSS_Context::$user_conf->since_hours_posts_per_rss;
+ }
?>
<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($url_rss); ?>" />
<?php } if (FreshRSS_Context::$system_conf->allow_robots) { ?>
diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml
index 23255f04f..f6d824d55 100644
--- a/app/layout/nav_menu.phtml
+++ b/app/layout/nav_menu.phtml
@@ -83,7 +83,7 @@
<div class="stick" id="nav_menu_read_all">
<form id="mark-read-menu" method="post">
- <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>
+ <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>
<button class="read_all btn <?php echo $confirm; ?>"
form="mark-read-menu"
formaction="<?php echo Minz_Url::display($mark_read_url); ?>"
@@ -151,8 +151,11 @@
if (FreshRSS_Context::$user_conf->token) {
$url_output['params']['token'] = FreshRSS_Context::$user_conf->token;
}
+ if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
+ $url_output['params']['hours'] = FreshRSS_Context::$user_conf->since_hours_posts_per_rss;
+ }
?>
- <a class="view_rss btn" target="_blank" title="<?php echo _t('index.menu.rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+ <a class="view_rss btn" target="_blank" rel="noreferrer" title="<?php echo _t('index.menu.rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
<?php echo _i('rss'); ?>
</a>
</div>
diff --git a/app/views/auth/index.phtml b/app/views/auth/index.phtml
index 74e692ec5..010eae33f 100644
--- a/app/views/auth/index.phtml
+++ b/app/views/auth/index.phtml
@@ -60,7 +60,7 @@
<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo _t('gen.short.blank_to_disable'); ?>"<?php
echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?php echo $token; ?>"/>
<?php echo _i('help'); ?> <?php echo _t('admin.auth.token_help'); ?>
- <kbd><?php echo Minz_Url::display(array('params' => array('output' => 'rss', 'token' => $token)), 'html', true); ?></kbd>
+ <kbd><?php echo Minz_Url::display(array('a' => 'rss', 'params' => array('token' => $token, 'hours' => FreshRSS_Context::$user_conf->since_hours_posts_per_rss)), 'html', true); ?></kbd>
</div>
</div>
<?php } ?>
diff --git a/app/views/configure/sharing.phtml b/app/views/configure/sharing.phtml
index 0dad5bf6d..ffcfb8b29 100644
--- a/app/views/configure/sharing.phtml
+++ b/app/views/configure/sharing.phtml
@@ -13,7 +13,7 @@
<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_name'); ?>" size="64" />
<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_url'); ?>" size="64" />
<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo _i('close'); ?></a></div>
- <a target="_blank" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a>
+ <a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a>
</div></div>'>
<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
<legend><?php echo _t('conf.sharing'); ?></legend>
@@ -38,7 +38,7 @@
<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo _i('close'); ?></a>
</div>
<?php if ($share->formType() === 'advanced') { ?>
- <a target="_blank" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a>
+ <a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a>
<?php } ?>
</div>
</div>
diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml
index caf685d79..5cd59d298 100644
--- a/app/views/feed/add.phtml
+++ b/app/views/feed/add.phtml
@@ -30,7 +30,7 @@
<label class="group-name"><?php echo _t('sub.feed.website'); ?></label>
<div class="group-controls">
<?php echo $this->feed->website(); ?>
- <a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
+ <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
</div>
</div>
<?php } ?>
@@ -40,9 +40,9 @@
<div class="group-controls">
<div class="stick">
<input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
- <a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
+ <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
</div>
- <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo _t('sub.feed.validator'); ?></a>
+ <a class="btn" target="_blank" rel="noreferrer" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo _t('sub.feed.validator'); ?></a>
</div>
</div>
<div class="form-group">
diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml
index b7e8f68cd..bf87a255a 100644
--- a/app/views/helpers/feed/update.phtml
+++ b/app/views/helpers/feed/update.phtml
@@ -37,7 +37,7 @@
<div class="group-controls">
<div class="stick">
<input type="text" name="website" id="website" class="extend" value="<?php echo $this->feed->website(); ?>" />
- <a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
+ <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a>
</div>
</div>
</div>
@@ -46,10 +46,10 @@
<div class="group-controls">
<div class="stick">
<input type="text" name="url" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
- <a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
+ <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a>
</div>
- <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo rawurlencode(htmlspecialchars_decode($this->feed->url(), ENT_QUOTES)); ?>"><?php echo _t('sub.feed.validator'); ?></a>
+ <a class="btn" target="_blank" rel="noreferrer" href="http://validator.w3.org/feed/check.cgi?url=<?php echo rawurlencode(htmlspecialchars_decode($this->feed->url(), ENT_QUOTES)); ?>"><?php echo _t('sub.feed.validator'); ?></a>
</div>
</div>
<div class="form-group">
diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml
index 3af7436c3..a9d5a80ca 100644
--- a/app/views/helpers/index/normal/entry_bottom.phtml
+++ b/app/views/helpers/index/normal/entry_bottom.phtml
@@ -52,7 +52,7 @@
$share_options['title'] = $title;
$share->update($share_options);
?><li class="item share">
- <a target="_blank" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a>
+ <a target="_blank" rel="noreferrer" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a>
</li><?php
}
?></ul>
@@ -81,6 +81,6 @@
?><li class="item date"><?php echo $this->entry->date(); ?></li><?php
}
if ($bottomline_link) {
- ?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php
+ ?><li class="item link"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php
} ?>
</ul>
diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml
index dc544298f..86298e59f 100644
--- a/app/views/helpers/index/normal/entry_header.phtml
+++ b/app/views/helpers/index/normal/entry_header.phtml
@@ -27,7 +27,7 @@
}
}
?><li class="item website"><a href="<?php echo _url('index', 'index', 'get', 'f_' . $this->feed->id()); ?>"><img class="favicon" src="<?php echo $this->feed->favicon(); ?>" alt="✇" /> <span><?php echo $this->feed->name(); ?></span></a></li>
- <li class="item title"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></li>
+ <li class="item title"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></li>
<?php if ($topline_date) { ?><li class="item date"><?php echo $this->entry->date(); ?> </li><?php } ?>
- <?php if ($topline_link) { ?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?>
+ <?php if ($topline_link) { ?><li class="item link"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?>
</ul>
diff --git a/app/views/helpers/pagination.phtml b/app/views/helpers/pagination.phtml
index 20957fc67..893451af9 100755
--- a/app/views/helpers/pagination.phtml
+++ b/app/views/helpers/pagination.phtml
@@ -1,6 +1,7 @@
<?php
$url_next = Minz_Request::currentRequest();
$url_next['params']['next'] = FreshRSS_Context::$next_id;
+ $url_next['params']['state'] = FreshRSS_Context::$state;
$url_next['params']['ajax'] = 1;
$url_mark_read = array(
@@ -26,7 +27,7 @@
</a>
<?php } elseif ($url_mark_read) { ?>
<button id="bigMarkAsRead"
- class="as-link <?php echo FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>"
+ class="as-link <?php echo FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>"
form="mark-read-pagination"
formaction="<?php echo Minz_Url::display($url_mark_read); ?>"
type="submit">
diff --git a/app/views/importExport/index.phtml b/app/views/importExport/index.phtml
index c0bc412c3..c5049e3ea 100644
--- a/app/views/importExport/index.phtml
+++ b/app/views/importExport/index.phtml
@@ -44,7 +44,7 @@
$select_args = ' size="' . min(10, count($this->feeds)) .'" multiple="multiple"';
}
?>
- <select name="export_feeds[]"<?php echo $select_args; ?>>
+ <select name="export_feeds[]"<?php echo $select_args; ?> size="10">
<?php echo extension_loaded('zip') ? '' : '<option></option>'; ?>
<?php foreach ($this->feeds as $feed) { ?>
<option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option>
diff --git a/app/views/index/global.phtml b/app/views/index/global.phtml
index 1e53e4f8c..f35732c8f 100644
--- a/app/views/index/global.phtml
+++ b/app/views/index/global.phtml
@@ -11,10 +11,13 @@
<div id="stream" class="global<?php echo $class; ?>">
<?php
+ $params = Minz_Request::fetchGET();
+ unset($params['c']);
+ unset($params['a']);
$url_base = array(
'c' => 'index',
'a' => 'normal',
- 'params' => Minz_Request::fetchGET(),
+ 'params' => $params,
);
foreach ($this->categories as $cat) {
diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml
index 91ebcebd3..6fda11ed9 100644
--- a/app/views/index/normal.phtml
+++ b/app/views/index/normal.phtml
@@ -66,7 +66,7 @@ if (!empty($this->entries)) {
?><div class="flux_content">
<div class="content <?php echo $content_width; ?>">
- <h1 class="title"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1>
+ <h1 class="title"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1>
<?php
$author = $this->entry->author();
echo $author != '' ? '<div class="author">' . _t('gen.short.by_author', $author) . '</div>' : '',
diff --git a/app/views/stats/index.phtml b/app/views/stats/index.phtml
index 0a2fbdb10..a36f812a8 100644
--- a/app/views/stats/index.phtml
+++ b/app/views/stats/index.phtml
@@ -23,18 +23,18 @@
</tr>
<tr>
<th><?php echo _t('admin.stats.status_read'); ?></th>
- <td class="numeric"><?php echo format_number($this->repartition['main_stream']['read']); ?></td>
- <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['read']); ?></td>
+ <td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_reads']); ?></td>
+ <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_reads']); ?></td>
</tr>
<tr>
<th><?php echo _t('admin.stats.status_unread'); ?></th>
- <td class="numeric"><?php echo format_number($this->repartition['main_stream']['unread']); ?></td>
- <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['unread']); ?></td>
+ <td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_unreads']); ?></td>
+ <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_unreads']); ?></td>
</tr>
<tr>
<th><?php echo _t('admin.stats.status_favorites'); ?></th>
- <td class="numeric"><?php echo format_number($this->repartition['main_stream']['favorite']); ?></td>
- <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['favorite']); ?></td>
+ <td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_favorites']); ?></td>
+ <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_favorites']); ?></td>
</tr>
</tbody>
</table>
diff --git a/app/views/stats/repartition.phtml b/app/views/stats/repartition.phtml
index ffb2c361e..5ebcdce5a 100644
--- a/app/views/stats/repartition.phtml
+++ b/app/views/stats/repartition.phtml
@@ -12,7 +12,7 @@
if (!empty($feeds)) {
echo '<optgroup label="', $category->name(), '">';
foreach ($feeds as $feed) {
- if ($this->feed && $feed->id() == $this->feed->id()){
+ if ($this->feed && $feed->id() == $this->feed->id()) {
echo '<option value="', $feed->id(), '" selected="selected" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
} else {
echo '<option value="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
@@ -39,9 +39,9 @@
</tr>
<tr>
<td class="numeric"><?php echo $this->repartition['total']; ?></td>
- <td class="numeric"><?php echo $this->repartition['read']; ?></td>
- <td class="numeric"><?php echo $this->repartition['unread']; ?></td>
- <td class="numeric"><?php echo $this->repartition['favorite']; ?></td>
+ <td class="numeric"><?php echo $this->repartition['count_reads']; ?></td>
+ <td class="numeric"><?php echo $this->repartition['count_unreads']; ?></td>
+ <td class="numeric"><?php echo $this->repartition['count_favorites']; ?></td>
</tr>
</table>
</div>
diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml
index aab3aa4c4..a32247d14 100644
--- a/app/views/user/manage.phtml
+++ b/app/views/user/manage.phtml
@@ -3,7 +3,7 @@
<div class="post">
<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a>
- <form method="post" action="<?php echo _url('user', 'create'); ?>">
+ <form method="post" action="<?php echo _url('user', 'create'); ?>" autocomplete="off">
<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
<legend><?php echo _t('admin.user.create'); ?></legend>
@@ -30,7 +30,7 @@
<label class="group-name" for="new_user_passwordPlain"><?php echo _t('admin.user.password_form'); ?></label>
<div class="group-controls">
<div class="stick">
- <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
+ <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="new-password" pattern=".{7,}" />
<a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
</div>
<?php echo _i('help'); ?> <?php echo _t('admin.user.password_format'); ?>
diff --git a/cli/.htaccess b/cli/.htaccess
new file mode 100644
index 000000000..9e768397d
--- /dev/null
+++ b/cli/.htaccess
@@ -0,0 +1,3 @@
+Order Allow,Deny
+Deny from all
+Satisfy all
diff --git a/cli/README.md b/cli/README.md
new file mode 100644
index 000000000..444606b50
--- /dev/null
+++ b/cli/README.md
@@ -0,0 +1,58 @@
+* Back to [main read-me](../README.md)
+
+# FreshRSS Command-Line Interface (CLI)
+
+## Note on access rights
+
+When using the command-line interface, remember that your user might not be the same as the one used by your Web server.
+This might create some access right problems.
+
+It is recommended to invoke commands using the same user as your Web server:
+
+```sh
+cd /usr/share/FreshRSS
+sudo -u www-data sh -c './cli/list-users.php'
+```
+
+In any case, when you are done with a series of commands, you should re-apply the access rights:
+
+```sh
+cd /usr/share/FreshRSS
+sudo chown -R :www-data .
+sudo chmod -R g+r .
+sudo chmod -R g+w ./data/
+```
+
+
+## Commands
+
+Options in parenthesis are optional.
+
+
+```sh
+cd /usr/share/FreshRSS
+
+./cli/do-install.php --default_user admin --auth_type form ( --environment production --base_url https://rss.example.net/ --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
+# --auth_type can be: 'form' (recommended), 'http_auth' (using the Web server access control), 'none' (dangerous)
+# --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)
+# --environment can be: 'production' (default), 'development' (for additional log messages)
+# --db-prefix is an optional prefix in front of the names of the tables
+# This command does not create the default user. Do that with ./cli/create-user.php
+
+./cli/create-user.php --user username ( --password 'password' --api-password 'api_password' --language en --email user@example.net --token 'longRandomString' --no-default-feeds )
+# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/)
+
+./cli/delete-user.php --user username
+
+./cli/list-users.php
+# Return a list of users, with the default/admin user first
+
+./cli/actualize-user.php --user username
+
+./cli/import-for-user.php --user username --filename /path/to/file.ext
+# The extension of the file { .json, .opml, .xml, .zip } is used to detect the type of import
+
+./cli/export-opml-for-user.php --user username > /path/to/file.opml.xml
+
+./cli/export-zip-for-user.php --user username ( --max-feed-entries 100 ) > /path/to/file.zip
+```
diff --git a/cli/_cli.php b/cli/_cli.php
new file mode 100644
index 000000000..7d1a7c6b2
--- /dev/null
+++ b/cli/_cli.php
@@ -0,0 +1,49 @@
+<?php
+if (php_sapi_name() !== 'cli') {
+ die('FreshRSS error: This PHP script may only be invoked from command line!');
+}
+
+require(dirname(__FILE__) . '/../constants.php');
+require(LIB_PATH . '/lib_rss.php');
+
+Minz_Configuration::register('system',
+ DATA_PATH . '/config.php',
+ DATA_PATH . '/config.default.php');
+FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+Minz_Translate::init('en');
+
+FreshRSS_Context::$isCli = true;
+
+function fail($message) {
+ fwrite(STDERR, $message . "\n");
+ die(1);
+}
+
+function cliInitUser($username) {
+ if (!ctype_alnum($username)) {
+ fail('FreshRSS error: invalid username: ' . $username . "\n");
+ }
+
+ $usernames = listUsers();
+ if (!in_array($username, $usernames)) {
+ fail('FreshRSS error: user not found: ' . $username . "\n");
+ }
+
+ FreshRSS_Context::$user_conf = get_user_configuration($username);
+ if (FreshRSS_Context::$user_conf == null) {
+ fail('FreshRSS error: invalid configuration for user: ' . $username . "\n");
+ }
+ new Minz_ModelPdo($username);
+
+ return $username;
+}
+
+function accessRights() {
+ echo '• Remember to re-apply the appropriate access rights, such as:' , "\n",
+ "\t", 'sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/', "\n";
+}
+
+function done($ok = true) {
+ fwrite(STDERR, 'Result: ' . ($ok ? 'success' : 'fail') . "\n");
+ exit($ok ? 0 : 1);
+}
diff --git a/cli/actualize-user.php b/cli/actualize-user.php
new file mode 100755
index 000000000..29d51753a
--- /dev/null
+++ b/cli/actualize-user.php
@@ -0,0 +1,23 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
+
+list($nbUpdatedFeeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
+
+echo "FreshRSS actualized $nbUpdatedFeeds feeds for $username\n";
+
+invalidateHttpCache($username);
+
+done($nbUpdatedFeeds > 0);
diff --git a/cli/create-user.php b/cli/create-user.php
new file mode 100755
index 000000000..008b82ce3
--- /dev/null
+++ b/cli/create-user.php
@@ -0,0 +1,48 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ 'password:',
+ 'api-password:',
+ 'language:',
+ 'email:',
+ 'token:',
+ 'no-default-feeds',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username ( --password 'password' --api-password 'api_password'" .
+ " --language en --email user@example.net --token 'longRandomString --no-default-feeds' )");
+}
+$username = $options['user'];
+if (!ctype_alnum($username)) {
+ fail('FreshRSS error: invalid username “' . $username . '”');
+}
+
+$usernames = listUsers();
+if (preg_grep("/^$username$/i", $usernames)) {
+ fail('FreshRSS error: username already taken “' . $username . '”');
+}
+
+echo 'FreshRSS creating user “', $username, "”…\n";
+
+$ok = FreshRSS_user_Controller::createUser($username,
+ empty($options['password']) ? '' : $options['password'],
+ empty($options['api-password']) ? '' : $options['api-password'],
+ array(
+ 'language' => empty($options['language']) ? '' : $options['language'],
+ 'token' => empty($options['token']) ? '' : $options['token'],
+ ),
+ !isset($options['no-default-feeds']));
+
+if (!$ok) {
+ fail('FreshRSS could not create user!');
+}
+
+invalidateHttpCache(FreshRSS_Context::$system_conf->default_user);
+
+accessRights();
+
+done($ok);
diff --git a/cli/delete-user.php b/cli/delete-user.php
new file mode 100755
index 000000000..6f0e86e17
--- /dev/null
+++ b/cli/delete-user.php
@@ -0,0 +1,32 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username");
+}
+$username = $options['user'];
+if (!ctype_alnum($username)) {
+ fail('FreshRSS error: invalid username “' . $username . '”');
+}
+
+$usernames = listUsers();
+if (!preg_grep("/^$username$/i", $usernames)) {
+ fail('FreshRSS error: username not found “' . $username . '”');
+}
+
+if (strcasecmp($username, FreshRSS_Context::$system_conf->default_user) === 0) {
+ fail('FreshRSS error: default user must not be deleted: “' . $username . '”');
+}
+
+echo 'FreshRSS deleting user “', $username, "”…\n";
+
+$ok = FreshRSS_user_Controller::deleteUser($username);
+
+invalidateHttpCache(FreshRSS_Context::$system_conf->default_user);
+
+done($ok);
diff --git a/cli/do-install.php b/cli/do-install.php
new file mode 100755
index 000000000..667191680
--- /dev/null
+++ b/cli/do-install.php
@@ -0,0 +1,101 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+require(LIB_PATH . '/lib_install.php');
+
+$params = array(
+ 'environment:',
+ 'base_url:',
+ 'title:',
+ 'default_user:',
+ 'allow_anonymous',
+ 'allow_anonymous_refresh',
+ 'auth_type:',
+ 'api_enabled',
+ 'allow_robots',
+ );
+
+$dBparams = array(
+ 'db-type:',
+ 'db-host:',
+ 'db-user:',
+ 'db-password:',
+ 'db-base:',
+ 'db-prefix:',
+ );
+
+$options = getopt('', array_merge($params, $dBparams));
+
+if (empty($options['default_user']) || empty($options['auth_type'])) {
+ fail('Usage: ' . basename(__FILE__) . " --default_user admin --auth_type form" .
+ " ( --environment production --base_url https://rss.example.net/" .
+ " --title FreshRSS --allow_anonymous --api_enabled" .
+ " --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
+ " --db-base freshrss --db-prefix freshrss )");
+}
+
+fwrite(STDERR, 'FreshRSS install…' . "\n");
+
+$requirements = checkRequirements();
+if ($requirements['all'] !== 'ok') {
+ $message = 'FreshRSS install failed requirements:' . "\n";
+ foreach ($requirements as $requirement => $check) {
+ if ($check !== 'ok' && $requirement !== 'all') {
+ $message .= '• ' . $requirement . "\n";
+ }
+ }
+ fail($message);
+}
+
+if (!ctype_alnum($options['default_user'])) {
+ fail('FreshRSS invalid default username (must be ASCII alphanumeric): ' . $options['default_user']);
+}
+
+if (!in_array($options['auth_type'], array('form', 'http_auth', 'none'))) {
+ fail('FreshRSS invalid authentication method (auth_type must be one of { form, http_auth, none }: ' . $options['auth_type']);
+}
+
+$config = array(
+ 'salt' => generateSalt(),
+ 'db' => FreshRSS_Context::$system_conf->db,
+ );
+
+foreach ($params as $param) {
+ $param = rtrim($param, ':');
+ if (isset($options[$param])) {
+ $config[$param] = $options[$param] === false ? true : $options[$param];
+ }
+}
+
+if ((!empty($config['base_url'])) && server_is_public($config['base_url'])) {
+ $config['pubsubhubbub_enabled'] = true;
+}
+
+foreach ($dBparams as $dBparam) {
+ $dBparam = rtrim($dBparam, ':');
+ if (!empty($options[$dBparam])) {
+ $param = substr($dBparam, strlen('db-'));
+ $config['db'][$param] = $options[$dBparam];
+ }
+}
+
+if (file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config, true) . ";\n") === false) {
+ fail('FreshRSS could not write configuration file!: ' . join_path(DATA_PATH, 'config.php'));
+}
+
+$config['db']['default_user'] = $config['default_user'];
+if (!checkDb($config['db'])) {
+ @unlink(join_path(DATA_PATH, 'config.php'));
+ fail('FreshRSS database error: ' . (empty($config['db']['bd_error']) ? 'Unknown error' : $config['db']['bd_error']));
+}
+
+echo '• Remember to create the default user: ', $config['default_user'] , "\n",
+ "\t", './cli/create-user.php --user ', $config['default_user'] , " --password 'password' --more-options\n";
+
+accessRights();
+
+if (!deleteInstall()) {
+ fail('FreshRSS access right problem while deleting install file!');
+}
+
+done();
diff --git a/cli/export-opml-for-user.php b/cli/export-opml-for-user.php
new file mode 100755
index 000000000..95b12281f
--- /dev/null
+++ b/cli/export-opml-for-user.php
@@ -0,0 +1,24 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username > /path/to/file.opml.xml");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+$ok = $importController->exportFile(true, false, array(), 0, $username);
+
+invalidateHttpCache($username);
+
+done($ok);
diff --git a/cli/export-zip-for-user.php b/cli/export-zip-for-user.php
new file mode 100755
index 000000000..92fe9bf9a
--- /dev/null
+++ b/cli/export-zip-for-user.php
@@ -0,0 +1,30 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ 'max-feed-entries:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username ( --max-feed-entries 100 ) > /path/to/file.zip");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS exporting ZIP for user “' . $username . "”…\n");
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+try {
+ $ok = $importController->exportFile(true, true, true,
+ empty($options['max-feed-entries']) ? 100 : intval($options['max-feed-entries']),
+ $username);
+} catch (FreshRSS_ZipMissing_Exception $zme) {
+ fail('FreshRSS error: Lacking php-zip extension!');
+}
+invalidateHttpCache($username);
+
+done($ok);
diff --git a/cli/import-for-user.php b/cli/import-for-user.php
new file mode 100755
index 000000000..29084f062
--- /dev/null
+++ b/cli/import-for-user.php
@@ -0,0 +1,35 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ 'filename:',
+ ));
+
+if (empty($options['user']) || empty($options['filename'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username --filename /path/to/file.ext");
+}
+
+$username = cliInitUser($options['user']);
+
+$filename = $options['filename'];
+if (!is_readable($filename)) {
+ fail('FreshRSS error: file is not readable “' . $filename . '”');
+}
+
+echo 'FreshRSS importing ZIP/OPML/JSON for user “', $username, "”…\n";
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+try {
+ $ok = $importController->importFile($filename, $filename, $username);
+} catch (FreshRSS_ZipMissing_Exception $zme) {
+ fail('FreshRSS error: Lacking php-zip extension!');
+} catch (FreshRSS_Zip_Exception $ze) {
+ fail('FreshRSS error: ZIP archive cannot be imported! Error code: ' . $ze->zipErrorCode());
+}
+invalidateHttpCache($username);
+
+done($ok);
diff --git a/data/persona/index.html b/cli/index.html
index 85faaa37e..85faaa37e 100644
--- a/data/persona/index.html
+++ b/cli/index.html
diff --git a/cli/list-users.php b/cli/list-users.php
new file mode 100755
index 000000000..e690ff451
--- /dev/null
+++ b/cli/list-users.php
@@ -0,0 +1,14 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$users = listUsers();
+sort($users);
+if (FreshRSS_Context::$system_conf->default_user !== '') {
+ array_unshift($users, FreshRSS_Context::$system_conf->default_user);
+ $users = array_unique($users);
+}
+
+foreach ($users as $user) {
+ echo $user, "\n";
+}
diff --git a/constants.php b/constants.php
index 80a22c8f0..08268fdc3 100644
--- a/constants.php
+++ b/constants.php
@@ -1,5 +1,5 @@
<?php
-define('FRESHRSS_VERSION', '1.5.0');
+define('FRESHRSS_VERSION', '1.6.0');
define('FRESHRSS_WEBSITE', 'http://freshrss.org');
define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
diff --git a/data/.gitignore b/data/.gitignore
index c2ed350a6..76314fc12 100644
--- a/data/.gitignore
+++ b/data/.gitignore
@@ -1,10 +1,6 @@
-application.ini
config.php
-*.sqlite
-touch.txt
-no-cache.txt
-*.bak.php
-*.lock.txt
+config.php.bak.php
+force-https.txt
last_update.txt
+no-cache.txt
update.php
-force-https.txt
diff --git a/data/PubSubHubbub/feeds/.gitignore b/data/PubSubHubbub/feeds/.gitignore
index 150f68c80..d8f9df042 100644
--- a/data/PubSubHubbub/feeds/.gitignore
+++ b/data/PubSubHubbub/feeds/.gitignore
@@ -1 +1,3 @@
-*/*
+*/
+*/*.json
+*/*.txt
diff --git a/data/config.default.php b/data/config.default.php
index af1482d03..d3cd3bf22 100644
--- a/data/config.default.php
+++ b/data/config.default.php
@@ -27,6 +27,9 @@ return array(
# Title of this FreshRSS instance in the Web user interface.
'title' => 'FreshRSS',
+ # Meta description used when `allow_robots` is true.
+ 'meta_description' => '',
+
# Name of the user that has administration rights.
'default_user' => '_',
diff --git a/data/persona/.gitignore b/data/persona/.gitignore
deleted file mode 100644
index 314f02b1b..000000000
--- a/data/persona/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.txt \ No newline at end of file
diff --git a/data/users/.gitignore b/data/users/.gitignore
index a8b7cd60f..3705c06b7 100644
--- a/data/users/.gitignore
+++ b/data/users/.gitignore
@@ -1,4 +1,5 @@
-db.sqlite
-config.php
-log*.txt
-
+*/
+*/config.php
+*/db.sqlite
+!_/
+*/log*.txt
diff --git a/data/users/_/config.default.php b/data/users/_/config.default.php
index 4a3403453..f28ef9724 100644
--- a/data/users/_/config.default.php
+++ b/data/users/_/config.default.php
@@ -3,12 +3,16 @@
return array (
'language' => 'en',
'old_entries' => 3,
- 'keep_history_default' => 0,
+ 'keep_history_default' => 50,
'ttl_default' => 3600,
+ 'mail_login' => '',
'token' => '',
'passwordHash' => '',
'apiPasswordHash' => '',
'posts_per_page' => 20,
+ 'since_hours_posts_per_rss' => 168,
+ 'min_posts_per_rss' => 2,
+ 'max_posts_per_rss' => 400,
'view_mode' => 'normal',
'default_view' => 'adaptive',
'default_state' => FreshRSS_Entry::STATE_NOT_READ,
@@ -31,7 +35,7 @@ return array (
'mark_when' => array (
'article' => true,
'site' => true,
- 'scroll' => false,
+ 'scroll' => true,
'reception' => false,
),
'theme' => 'Origine',
diff --git a/lib/Favicon/DataAccess.php b/lib/Favicon/DataAccess.php
index 17f26b333..ae7509881 100644
--- a/lib/Favicon/DataAccess.php
+++ b/lib/Favicon/DataAccess.php
@@ -16,7 +16,7 @@ class DataAccess {
public function retrieveHeader($url) {
$this->set_context();
$headers = @get_headers($url, 1);
- return array_change_key_case($headers);
+ return $headers ? array_change_key_case($headers) : array();
}
public function saveCache($file, $data) {
diff --git a/lib/Minz/Log.php b/lib/Minz/Log.php
index 2a9e10993..9559a0bd4 100644
--- a/lib/Minz/Log.php
+++ b/lib/Minz/Log.php
@@ -42,7 +42,11 @@ class Minz_Log {
|| ($env === 'production'
&& ($level >= Minz_Log::NOTICE)))) {
if ($file_name === null) {
- $file_name = join_path(USERS_PATH, Minz_Session::param('currentUser', '_'), 'log.txt');
+ $username = Minz_Session::param('currentUser', '');
+ if ($username == '') {
+ $username = '_';
+ }
+ $file_name = join_path(USERS_PATH, $username, 'log.txt');
}
switch ($level) {
diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php
index 845aecaae..6e8d60bc9 100644
--- a/lib/Minz/ModelPdo.php
+++ b/lib/Minz/ModelPdo.php
@@ -36,55 +36,61 @@ class Minz_ModelPdo {
* HOST, BASE, USER et PASS définies dans le fichier de configuration
*/
public function __construct($currentUser = null) {
- if (self::$useSharedBd && self::$sharedBd != null && $currentUser === null) {
+ if ($currentUser === null) {
+ $currentUser = Minz_Session::param('currentUser');
+ }
+ if (self::$useSharedBd && self::$sharedBd != null &&
+ ($currentUser == null || $currentUser === self::$sharedCurrentUser)) {
$this->bd = self::$sharedBd;
$this->prefix = self::$sharedPrefix;
$this->current_user = self::$sharedCurrentUser;
return;
}
+ $this->current_user = $currentUser;
+ self::$sharedCurrentUser = $currentUser;
$conf = Minz_Configuration::get('system');
$db = $conf->db;
- if ($currentUser === null) {
- $currentUser = Minz_Session::param('currentUser', '_');
- }
- $this->current_user = $currentUser;
- self::$sharedCurrentUser = $currentUser;
-
$driver_options = isset($conf->db['pdo_options']) && is_array($conf->db['pdo_options']) ? $conf->db['pdo_options'] : array();
+ $dbServer = parse_url('db://' . $db['host']);
try {
- $type = $db['type'];
- if ($type === 'mysql') {
- $string = 'mysql:host=' . $db['host']
- . ';dbname=' . $db['base']
- . ';charset=utf8mb4';
- $driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4';
- $this->prefix = $db['prefix'] . $currentUser . '_';
- } elseif ($type === 'sqlite') {
- $string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
- //$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
- $this->prefix = '';
- } else {
- throw new Minz_PDOConnectionException(
- 'Invalid database type!',
- $db['user'], Minz_Exception::ERROR
- );
- }
- self::$sharedDbType = $type;
- self::$sharedPrefix = $this->prefix;
-
- $this->bd = new MinzPDO(
- $string,
- $db['user'],
- $db['password'],
- $driver_options
- );
- if ($type === 'sqlite') {
- $this->bd->exec('PRAGMA foreign_keys = ON;');
+ switch ($db['type']) {
+ case 'mysql':
+ $string = 'mysql:host=' . $dbServer['host'] . ';dbname=' . $db['base'] . ';charset=utf8mb4';
+ if (!empty($dbServer['port'])) {
+ $string .= ';port=' . $dbServer['port'];
+ }
+ $driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4';
+ $this->prefix = $db['prefix'] . $currentUser . '_';
+ $this->bd = new MinzPDOMySql($string, $db['user'], $db['password'], $driver_options);
+ break;
+ case 'sqlite':
+ $string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
+ $this->prefix = '';
+ $this->bd = new MinzPDOMSQLite($string, $db['user'], $db['password'], $driver_options);
+ $this->bd->exec('PRAGMA foreign_keys = ON;');
+ break;
+ case 'pgsql':
+ $string = 'pgsql:host=' . $dbServer['host'] . ';dbname=' . $db['base'];
+ if (!empty($dbServer['port'])) {
+ $string .= ';port=' . $dbServer['port'];
+ }
+ $this->prefix = $db['prefix'] . $currentUser . '_';
+ $this->bd = new MinzPDOPGSQL($string, $db['user'], $db['password'], $driver_options);
+ $this->bd->exec("SET NAMES 'UTF8';");
+ break;
+ default:
+ throw new Minz_PDOConnectionException(
+ 'Invalid database type!',
+ $db['user'], Minz_Exception::ERROR
+ );
+ break;
}
self::$sharedBd = $this->bd;
+ self::$sharedDbType = $db['type'];
+ self::$sharedPrefix = $this->prefix;
} catch (Exception $e) {
throw new Minz_PDOConnectionException(
$string,
@@ -119,18 +125,43 @@ class MinzPDO extends PDO {
}
}
+ protected function compatibility($statement) {
+ return $statement;
+ }
+
public function prepare($statement, $driver_options = array()) {
MinzPDO::check($statement);
+ $statement = $this->compatibility($statement);
return parent::prepare($statement, $driver_options);
}
public function exec($statement) {
MinzPDO::check($statement);
+ $statement = $this->compatibility($statement);
return parent::exec($statement);
}
public function query($statement) {
MinzPDO::check($statement);
+ $statement = $this->compatibility($statement);
return parent::query($statement);
}
}
+
+class MinzPDOMySql extends MinzPDO {
+ public function lastInsertId($name = null) {
+ return parent::lastInsertId(); //We discard the name, only used by PostgreSQL
+ }
+}
+
+class MinzPDOMSQLite extends MinzPDO {
+ public function lastInsertId($name = null) {
+ return parent::lastInsertId(); //We discard the name, only used by PostgreSQL
+ }
+}
+
+class MinzPDOPGSQL extends MinzPDO {
+ protected function compatibility($statement) {
+ return str_replace(array('`', ' LIKE '), array('"', ' ILIKE '), $statement);
+ }
+}
diff --git a/lib/Minz/Url.php b/lib/Minz/Url.php
index c7c67123e..99c0443c1 100644
--- a/lib/Minz/Url.php
+++ b/lib/Minz/Url.php
@@ -78,6 +78,8 @@ class Minz_Url {
}
if (isset($url['params'])) {
+ unset($url['params']['c']);
+ unset($url['params']['a']);
foreach ($url['params'] as $key => $param) {
$uri .= $separator . urlencode($key) . '=' . urlencode($param);
$separator = $and;
diff --git a/lib/SimplePie/SimplePie/Content/Type/Sniffer.php b/lib/SimplePie/SimplePie/Content/Type/Sniffer.php
index a32f47f59..ec0bf0952 100644
--- a/lib/SimplePie/SimplePie/Content/Type/Sniffer.php
+++ b/lib/SimplePie/SimplePie/Content/Type/Sniffer.php
@@ -109,9 +109,7 @@ class SimplePie_Content_Type_Sniffer
{
return $this->unknown();
}
- elseif (substr($official, -4) === '+xml'
- || $official === 'text/xml'
- || $official === 'application/xml')
+ elseif (substr($official, -4) === '+xml')
{
return $official;
}
@@ -126,7 +124,9 @@ class SimplePie_Content_Type_Sniffer
return $official;
}
}
- elseif ($official === 'text/html')
+ elseif ($official === 'text/html'
+ || $official === 'text/xml'
+ || $official === 'application/xml')
{
return $this->feed_or_html();
}
@@ -256,7 +256,12 @@ class SimplePie_Content_Type_Sniffer
public function feed_or_html()
{
$len = strlen($this->file->body);
- $pos = strspn($this->file->body, "\x09\x0A\x0D\x20");
+ $pos = 0;
+ if (isset($this->file->body[2]) && $this->file->body[0] === "\xEF" &&
+ $this->file->body[1] === "\xBB" && $this->file->body[2] === "\xBF") {
+ $pos += 3; //UTF-8 BOM
+ }
+ $pos += strspn($this->file->body, "\x09\x0A\x0D\x20", $pos);
while ($pos < $len)
{
diff --git a/lib/favicons.php b/lib/favicons.php
new file mode 100644
index 000000000..6709f6745
--- /dev/null
+++ b/lib/favicons.php
@@ -0,0 +1,40 @@
+<?php
+
+include(LIB_PATH . '/Favicon/Favicon.php');
+include(LIB_PATH . '/Favicon/DataAccess.php');
+
+$favicons_dir = DATA_PATH . '/favicons/';
+$default_favicon = PUBLIC_PATH . '/themes/icons/default_favicon.ico';
+
+function download_favicon($website, $dest) {
+ global $favicons_dir, $default_favicon;
+
+ syslog(LOG_DEBUG, 'FreshRSS Favicon discovery GET ' . $website);
+ $favicon_getter = new \Favicon\Favicon();
+ $favicon_getter->setCacheDir($favicons_dir);
+ $favicon_url = $favicon_getter->get($website);
+
+ if ($favicon_url === false) {
+ return @copy($default_favicon, $dest);
+ }
+
+ syslog(LOG_DEBUG, 'FreshRSS Favicon GET ' . $favicon_url);
+ $c = curl_init($favicon_url);
+ curl_setopt($c, CURLOPT_HEADER, false);
+ curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($c, CURLOPT_BINARYTRANSFER, true);
+ $img_raw = curl_exec($c);
+ $status_code = curl_getinfo($c, CURLINFO_HTTP_CODE);
+ curl_close($c);
+
+ if ($status_code === 200) {
+ $file = fopen($dest, 'w');
+ if ($file !== false) {
+ fwrite($file, $img_raw);
+ fclose($file);
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/lib/lib_install.php b/lib/lib_install.php
new file mode 100644
index 000000000..0e7b7f036
--- /dev/null
+++ b/lib/lib_install.php
@@ -0,0 +1,115 @@
+<?php
+
+define('BCRYPT_COST', 9);
+
+Minz_Configuration::register('default_system', join_path(DATA_PATH, 'config.default.php'));
+Minz_Configuration::register('default_user', join_path(USERS_PATH, '_', 'config.default.php'));
+
+function checkRequirements() {
+ $php = version_compare(PHP_VERSION, '5.3.3') >= 0;
+ $minz = file_exists(join_path(LIB_PATH, 'Minz'));
+ $curl = extension_loaded('curl');
+ $pdo_mysql = extension_loaded('pdo_mysql');
+ $pdo_sqlite = extension_loaded('pdo_sqlite');
+ $pdo_pgsql = extension_loaded('pdo_pgsql');
+ $pdo = $pdo_mysql || $pdo_sqlite || $pdo_pgsql;
+ $pcre = extension_loaded('pcre');
+ $ctype = extension_loaded('ctype');
+ $dom = class_exists('DOMDocument');
+ $xml = function_exists('xml_parser_create');
+ $json = function_exists('json_encode');
+ $data = DATA_PATH && is_writable(DATA_PATH);
+ $cache = CACHE_PATH && is_writable(CACHE_PATH);
+ $users = USERS_PATH && is_writable(USERS_PATH);
+ $favicons = is_writable(join_path(DATA_PATH, 'favicons'));
+ $http_referer = is_referer_from_same_domain();
+
+ return array(
+ 'php' => $php ? 'ok' : 'ko',
+ 'minz' => $minz ? 'ok' : 'ko',
+ 'curl' => $curl ? 'ok' : 'ko',
+ 'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
+ 'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
+ 'pdo-pgsql' => $pdo_pgsql ? 'ok' : 'ko',
+ 'pdo' => $pdo ? 'ok' : 'ko',
+ 'pcre' => $pcre ? 'ok' : 'ko',
+ 'ctype' => $ctype ? 'ok' : 'ko',
+ 'dom' => $dom ? 'ok' : 'ko',
+ 'xml' => $xml ? 'ok' : 'ko',
+ 'json' => $json ? 'ok' : 'ko',
+ 'data' => $data ? 'ok' : 'ko',
+ 'cache' => $cache ? 'ok' : 'ko',
+ 'users' => $users ? 'ok' : 'ko',
+ 'favicons' => $favicons ? 'ok' : 'ko',
+ 'http_referer' => $http_referer ? 'ok' : 'ko',
+ 'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $xml &&
+ $data && $cache && $users && $favicons && $http_referer ?
+ 'ok' : 'ko'
+ );
+}
+
+function generateSalt() {
+ return sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
+}
+
+function checkDb(&$dbOptions) {
+ $dsn = '';
+ try {
+ $driver_options = null;
+ switch ($dbOptions['type']) {
+ case 'mysql':
+ include_once(APP_PATH . '/SQL/install.sql.mysql.php');
+ $driver_options = array(
+ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'
+ );
+ try { // on ouvre une connexion juste pour créer la base si elle n'existe pas
+ $dsn = 'mysql:host=' . $dbOptions['host'] . ';';
+ $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options);
+ $sql = sprintf(SQL_CREATE_DB, $dbOptions['base']);
+ $res = $c->query($sql);
+ } catch (PDOException $e) {
+ syslog(LOG_DEBUG, 'FreshRSS MySQL warning: ' . $e->getMessage());
+ }
+ // on écrase la précédente connexion en sélectionnant la nouvelle BDD
+ $dsn = 'mysql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['base'];
+ break;
+ case 'sqlite':
+ include_once(APP_PATH . '/SQL/install.sql.sqlite.php');
+ $dsn = 'sqlite:' . join_path(USERS_PATH, $dbOptions['default_user'], 'db.sqlite');
+ $driver_options = array(
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ );
+ break;
+ case 'pgsql':
+ include_once(APP_PATH . '/SQL/install.sql.pgsql.php');
+ $driver_options = array(
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ );
+ try { // on ouvre une connexion juste pour créer la base si elle n'existe pas
+ $dsn = 'pgsql:host=' . $dbOptions['host'] . ';dbname=postgres';
+ $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options);
+ $sql = sprintf(SQL_CREATE_DB, $dbOptions['base']);
+ $res = $c->query($sql);
+ } catch (PDOException $e) {
+ syslog(LOG_DEBUG, 'FreshRSS PostgreSQL warning: ' . $e->getMessage());
+ }
+ // on écrase la précédente connexion en sélectionnant la nouvelle BDD
+ $dsn = 'pgsql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['base'];
+ break;
+ default:
+ return false;
+ }
+ } catch (PDOException $e) {
+ $dsn = '';
+ $dbOptions['error'] = $e->getMessage();
+ }
+ $dbOptions['dsn'] = $dsn;
+ $dbOptions['options'] = $driver_options;
+ return $dsn != '';
+}
+
+function deleteInstall() {
+ $path = join_path(DATA_PATH, 'do-install.txt');
+ @unlink($path);
+ return !file_exists($path);
+}
diff --git a/lib/lib_rss.php b/lib/lib_rss.php
index 82ddced2c..143b55bee 100644
--- a/lib/lib_rss.php
+++ b/lib/lib_rss.php
@@ -83,6 +83,9 @@ function checkUrl($url) {
}
}
+function safe_ascii($text) {
+ return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
+}
/**
* Test if a given server address is publicly accessible.
@@ -165,7 +168,7 @@ function customSimplePie() {
$system_conf = Minz_Configuration::get('system');
$limits = $system_conf->limits;
$simplePie = new SimplePie();
- $simplePie->set_useragent(_t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION);
+ $simplePie->set_useragent('FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION);
$simplePie->set_syslog($system_conf->simplepie_syslog_enabled);
$simplePie->set_cache_location(CACHE_PATH);
$simplePie->set_cache_duration($limits['cache_duration']);
@@ -182,10 +185,9 @@ function customSimplePie() {
'onmouseover', 'onmousemove', 'onmouseout', 'onfocus', 'onblur',
'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange', 'seamless', 'sizes', 'srcset')));
$simplePie->add_attributes(array(
- 'img' => array('lazyload' => '', 'postpone' => ''), //http://www.w3.org/TR/resource-priorities/
- 'audio' => array('lazyload' => '', 'postpone' => '', 'preload' => 'none'),
- 'iframe' => array('lazyload' => '', 'postpone' => '', 'sandbox' => 'allow-scripts allow-same-origin'),
- 'video' => array('lazyload' => '', 'postpone' => '', 'preload' => 'none'),
+ 'audio' => array('preload' => 'none'),
+ 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'),
+ 'video' => array('preload' => 'none'),
));
$simplePie->set_url_replacements(array(
'a' => 'href',
@@ -280,9 +282,12 @@ function uSecString() {
return str_pad($t['usec'], 6, '0');
}
-function invalidateHttpCache() {
- Minz_Session::_param('touch', uTimeString());
- return touch(join_path(DATA_PATH, 'users', Minz_Session::param('currentUser', '_'), 'log.txt'));
+function invalidateHttpCache($username = '') {
+ if (($username == '') || (!ctype_alnum($username))) {
+ Minz_Session::_param('touch', uTimeString());
+ $username = Minz_Session::param('currentUser', '_');
+ }
+ return touch(join_path(DATA_PATH, 'users', $username, 'log.txt'));
}
function listUsers() {
diff --git a/p/api/greader.php b/p/api/greader.php
index 894c2e960..4965ffd3b 100644
--- a/p/api/greader.php
+++ b/p/api/greader.php
@@ -153,13 +153,12 @@ function authorizationToUser() {
if (count($headerAuthX) === 2) {
$user = $headerAuthX[0];
if (ctype_alnum($user)) {
- $conf = get_user_configuration($user);
- if (is_null($conf)) {
+ FreshRSS_Context::$user_conf = get_user_configuration($user);
+ if (FreshRSS_Context::$user_conf == null) {
Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.');
unauthorized();
}
- $system_conf = Minz_Configuration::get('system');
- if ($headerAuthX[1] === sha1($system_conf->salt . $user . $conf->apiPasswordHash)) {
+ if ($headerAuthX[1] === sha1(FreshRSS_Context::$system_conf->salt . $user . FreshRSS_Context::$user_conf->apiPasswordHash)) {
return $user;
} else {
logMe('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1]);
@@ -181,16 +180,15 @@ function clientLogin($email, $pass) { //http://web.archive.org/web/2013060409104
include_once(LIB_PATH . '/password_compat.php');
}
- $conf = get_user_configuration($email);
- if (is_null($conf)) {
+ FreshRSS_Context::$user_conf = get_user_configuration($email);
+ if (FreshRSS_Context::$user_conf == null) {
Minz_Log::warning('Invalid API user ' . $email . ': configuration cannot be found.');
unauthorized();
}
- if ($conf->apiPasswordHash != '' && password_verify($pass, $conf->apiPasswordHash)) {
+ if (FreshRSS_Context::$user_conf->apiPasswordHash != '' && password_verify($pass, FreshRSS_Context::$user_conf->apiPasswordHash)) {
header('Content-Type: text/plain; charset=UTF-8');
- $system_conf = Minz_Configuration::get('system');
- $auth = $email . '/' . sha1($system_conf->salt . $email . $conf->apiPasswordHash);
+ $auth = $email . '/' . sha1(FreshRSS_Context::$system_conf->salt . $email . FreshRSS_Context::$user_conf->apiPasswordHash);
echo 'SID=', $auth, "\n",
'Auth=', $auth, "\n";
exit();
@@ -209,8 +207,7 @@ function token($conf) {
//https://github.com/ericmann/gReader-Library/blob/master/greader.class.php
$user = Minz_Session::param('currentUser', '_');
//logMe('token('. $user . ")"); //TODO: Implement real token that expires
- $system_conf = Minz_Configuration::get('system');
- $token = str_pad(sha1($system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z'); //Must have 57 characters
+ $token = str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z'); //Must have 57 characters
echo $token, "\n";
exit();
}
@@ -219,13 +216,23 @@ function checkToken($conf, $token) {
//http://code.google.com/p/google-reader-api/wiki/ActionToken
$user = Minz_Session::param('currentUser', '_');
//logMe('checkToken(' . $token . ")");
- $system_conf = Minz_Configuration::get('system');
- if ($token === str_pad(sha1($system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) {
+ if ($token === str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) {
return true;
}
unauthorized();
}
+function userInfo() { //https://github.com/theoldreader/api#user-info
+ //logMe("userInfo()");
+ $user = Minz_Session::param('currentUser', '_');
+ exit(json_encode(array(
+ 'userId' => $user,
+ 'userName' => $user,
+ 'userProfileId' => $user,
+ 'userEmail' => FreshRSS_Context::$user_conf->mail_login,
+ )));
+}
+
function tagList() {
//logMe("tagList()");
header('Content-Type: application/json; charset=UTF-8');
@@ -261,6 +268,7 @@ function subscriptionList() {
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+ $salt = FreshRSS_Context::$system_conf->salt;
$subscriptions = array();
foreach ($res as $line) {
@@ -277,7 +285,7 @@ function subscriptionList() {
//'firstitemmsec' => 0,
'url' => $line['url'],
'htmlUrl' => $line['website'],
- //'iconUrl' => '',
+ 'iconUrl' => Minz_Url::display('/f.php?' . hash('crc32b', $salt . $line['url']), '', true),
);
}
@@ -285,6 +293,109 @@ function subscriptionList() {
exit();
}
+function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = '') {
+ //logMe("subscriptionEdit()");
+ //https://github.com/mihaip/google-reader-api/blob/master/wiki/ApiSubscriptionEdit.wiki
+ switch ($action) {
+ case 'subscribe':
+ case 'unsubscribe':
+ case 'edit':
+ break;
+ default:
+ badRequest();
+ }
+ $addCatId = 0;
+ $categoryDAO = null;
+ if ($add != '' || $remove != '') {
+ $categoryDAO = new FreshRSS_CategoryDAO();
+ }
+ $c_name = '';
+ if ($add != '' && strpos($add, 'user/') === 0) { //user/-/label/Example ; user/username/label/Example
+ if (strpos($add, 'user/-/label/') === 0) {
+ $c_name = substr($add, 13);
+ } else {
+ $user = Minz_Session::param('currentUser', '_');
+ $prefix = 'user/' . $user . '/label/';
+ if (strpos($add, $prefix) === 0) {
+ $c_name = substr($add, strlen($prefix));
+ } else {
+ $c_name = '';
+ }
+ }
+ $cat = $categoryDAO->searchByName($c_name);
+ $addCatId = $cat == null ? 0 : $cat->id();
+ } else if ($remove != '' && strpos($remove, 'user/-/label/')) {
+ $addCatId = 1; //Default category
+ }
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ for ($i = count($streamNames) - 1; $i >= 0; $i--) {
+ $streamName = $streamNames[$i]; //feed/http://example.net/sample.xml ; feed/338
+ if (strpos($streamName, 'feed/') === 0) {
+ $streamName = substr($streamName, 5);
+ $feedId = 0;
+ if (ctype_digit($streamName)) {
+ if ($action === 'subscribe') {
+ continue;
+ }
+ $feedId = $streamName;
+ } else {
+ $feed = $feedDAO->searchByUrl($streamName);
+ $feedId = $feed == null ? -1 : $feed->id();
+ }
+ $title = isset($titles[$i]) ? $titles[$i] : '';
+ switch ($action) {
+ case 'subscribe':
+ if ($feedId <= 0) {
+ $http_auth = ''; //TODO
+ try {
+ $feed = FreshRSS_feed_Controller::addFeed($streamName, $title, $addCatId, $c_name, $http_auth);
+ continue;
+ } catch (Exception $e) {
+ logMe("subscriptionEdit error subscribe: " . $e->getMessage());
+ }
+ }
+ badRequest();
+ break;
+ case 'unsubscribe':
+ if (!($feedId > 0 && FreshRSS_feed_Controller::deleteFeed($feedId))) {
+ badRequest();
+ }
+ break;
+ case 'edit':
+ if ($feedId > 0) {
+ if ($addCatId > 0 || $c_name != '') {
+ FreshRSS_feed_Controller::moveFeed($feedId, $addCatId, $c_name);
+ }
+ if ($title != '') {
+ FreshRSS_feed_Controller::renameFeed($feedId, $title);
+ }
+ } else {
+ badRequest();
+ }
+ break;
+ }
+ }
+ }
+ exit('OK');
+}
+
+function quickadd($url) {
+ //logMe("quickadd($url)");
+ try {
+ $feed = FreshRSS_feed_Controller::addFeed($url);
+ exit(json_encode(array(
+ 'numResults' => 1,
+ 'streamId' => $feed->id(),
+ )));
+ } catch (Exception $e) {
+ logMe("subscriptionEdit error subscribe: " . $e->getMessage());
+ die(json_encode(array(
+ 'numResults' => 0,
+ 'error' => $e->getMessage(),
+ )));
+ }
+}
+
function unreadCount() { //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#unread-count
//logMe("unreadCount()");
header('Content-Type: application/json; charset=UTF-8');
@@ -455,7 +566,7 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude
$id = basename($streamId);
} elseif (strpos($streamId, 'user/-/label/') === 0) {
$type = 'c';
- $c_name = basename($streamId);
+ $c_name = substr($streamId, 13);
$categoryDAO = new FreshRSS_CategoryDAO();
$cat = $categoryDAO->searchByName($c_name);
$id = $cat == null ? -1 : $cat->id();
@@ -521,8 +632,41 @@ function editTag($e_ids, $a, $r) {
break;
}
- echo 'OK';
- exit();
+ exit('OK');
+}
+
+function renameTag($s, $dest) {
+ //logMe("renameTag()");
+ if ($s != '' && strpos($s, 'user/-/label/') === 0 &&
+ $dest != '' && strpos($dest, 'user/-/label/') === 0) {
+ $s = substr($s, 13);
+ $categoryDAO = new FreshRSS_CategoryDAO();
+ $cat = $categoryDAO->searchByName($s);
+ if ($cat != null) {
+ $dest = substr($dest, 13);
+ $categoryDAO->updateCategory($cat->id(), array('name' => $dest));
+ exit('OK');
+ }
+ }
+ badRequest();
+}
+
+function disableTag($s) {
+ //logMe("disableTag($s)");
+ if ($s != '' && strpos($s, 'user/-/label/') === 0) {
+ $s = substr($s, 13);
+ $categoryDAO = new FreshRSS_CategoryDAO();
+ $cat = $categoryDAO->searchByName($s);
+ if ($cat != null) {
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feedDAO->changeCategory($cat->id(), 0);
+ if ($cat->id() > 1) {
+ $categoryDAO->deleteCategory($cat->id());
+ }
+ exit('OK');
+ }
+ }
+ badRequest();
}
function markAllAsRead($streamId, $olderThanId) {
@@ -532,7 +676,7 @@ function markAllAsRead($streamId, $olderThanId) {
$f_id = basename($streamId);
$entryDAO->markReadFeed($f_id, $olderThanId);
} elseif (strpos($streamId, 'user/-/label/') === 0) {
- $c_name = basename($streamId);
+ $c_name = substr($streamId, 13);
$categoryDAO = new FreshRSS_CategoryDAO();
$cat = $categoryDAO->searchByName($c_name);
$entryDAO->markReadCat($cat === null ? -1 : $cat->id(), $olderThanId);
@@ -540,8 +684,7 @@ function markAllAsRead($streamId, $olderThanId) {
$entryDAO->markReadEntries($olderThanId, false, -1);
}
- echo 'OK';
- exit();
+ exit('OK');
}
//logMe('----------------------------------------------------------------');
@@ -553,17 +696,17 @@ $pathInfos = explode('/', $pathInfo);
Minz_Configuration::register('system',
DATA_PATH . '/config.php',
DATA_PATH . '/config.default.php');
-$system_conf = Minz_Configuration::get('system');
-if (!$system_conf->api_enabled) {
+FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+if (!FreshRSS_Context::$system_conf->api_enabled) {
serviceUnavailable();
}
Minz_Session::init('FreshRSS');
$user = authorizationToUser();
-$conf = null;
+FreshRSS_Context::$user_conf = null;
if ($user !== '') {
- $conf = get_user_configuration($user);
+ FreshRSS_Context::$user_conf = get_user_configuration($user);
}
//logMe('User => ' . $user);
@@ -623,14 +766,33 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
if (isset($pathInfos[5]) && $pathInfos[5] === 'list') {
$output = isset($_GET['output']) ? $_GET['output'] : '';
if ($output !== 'json') notImplemented();
- tagList($_GET['output']);
+ tagList($output);
}
break;
case 'subscription':
- if (isset($pathInfos[5]) && $pathInfos[5] === 'list') {
- $output = isset($_GET['output']) ? $_GET['output'] : '';
- if ($output !== 'json') notImplemented();
- subscriptionList($_GET['output']);
+ if (isset($pathInfos[5])) {
+ switch ($pathInfos[5]) {
+ case 'list':
+ $output = isset($_GET['output']) ? $_GET['output'] : '';
+ if ($output !== 'json') notImplemented();
+ subscriptionList($_GET['output']);
+ break;
+ case 'edit':
+ if (isset($_POST['s']) && isset($_POST['ac'])) {
+ $streamNames = multiplePosts('s'); //StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once
+ $titles = multiplePosts('t'); //Title to use for the subscription. For the `subscribe` action, if not specified then the feed's current title will be used. Can be used with the `edit` action to rename a subscription
+ $action = $_POST['ac']; //Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit`
+ $add = isset($_POST['a']) ? $_POST['a'] : ''; //StreamId to add the subscription to (generally a user label)
+ $remove = isset($_POST['r']) ? $_POST['r'] : ''; //StreamId to remove the subscription from (generally a user label)
+ subscriptionEdit($streamNames, $titles, $action, $add, $remove);
+ }
+ break;
+ case 'quickadd': //https://github.com/theoldreader/api
+ if (isset($_GET['quickadd'])) {
+ quickadd($_GET['quickadd']);
+ }
+ break;
+ }
}
break;
case 'unread-count':
@@ -641,15 +803,30 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
break;
case 'edit-tag': //http://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3/
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
- checkToken($conf, $token);
+ checkToken(FreshRSS_Context::$user_conf, $token);
$a = isset($_POST['a']) ? $_POST['a'] : ''; //Add: user/-/state/com.google/read user/-/state/com.google/starred
$r = isset($_POST['r']) ? $_POST['r'] : ''; //Remove: user/-/state/com.google/read user/-/state/com.google/starred
$e_ids = multiplePosts('i'); //item IDs
editTag($e_ids, $a, $r);
break;
+ case 'rename-tag': //https://github.com/theoldreader/api
+ $token = isset($_POST['T']) ? trim($_POST['T']) : '';
+ checkToken(FreshRSS_Context::$user_conf, $token);
+ $s = isset($_POST['s']) ? $_POST['s'] : ''; //user/-/label/Folder
+ $dest = isset($_POST['dest']) ? $_POST['dest'] : ''; //user/-/label/NewFolder
+ renameTag($s, $dest);
+ break;
+ case 'disable-tag': //https://github.com/theoldreader/api
+ $token = isset($_POST['T']) ? trim($_POST['T']) : '';
+ checkToken(FreshRSS_Context::$user_conf, $token);
+ $s_s = multiplePosts('s');
+ foreach ($s_s as $s) {
+ disableTag($s); //user/-/label/Folder
+ }
+ break;
case 'mark-all-as-read':
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
- checkToken($conf, $token);
+ checkToken(FreshRSS_Context::$user_conf, $token);
$streamId = $_POST['s']; //StreamId
$ts = isset($_POST['ts']) ? $_POST['ts'] : '0'; //Older than timestamp in nanoseconds
if (!ctype_digit($ts)) {
@@ -658,7 +835,10 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
markAllAsRead($streamId, $ts);
break;
case 'token':
- token($conf);
+ token(FreshRSS_Context::$user_conf);
+ break;
+ case 'user-info':
+ userInfo();
break;
}
} elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {
diff --git a/p/api/pshb.php b/p/api/pshb.php
index 650767114..e9b66b167 100644
--- a/p/api/pshb.php
+++ b/p/api/pshb.php
@@ -88,9 +88,6 @@ if ($ORIGINAL_INPUT == '') {
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);
@@ -106,7 +103,6 @@ if ($self !== base64url_decode($canonical64)) {
//die('Self URL does not match registered canonical URL!');
$self = base64url_decode($canonical64);
}
-Minz_Request::_param('url', $self);
$nb = 0;
foreach ($users as $userFilename) {
@@ -120,8 +116,10 @@ foreach ($users as $userFilename) {
Minz_Configuration::register('user',
join_path(USERS_PATH, $username, 'config.php'),
join_path(USERS_PATH, '_', 'config.default.php'));
+ new Minz_ModelPdo($username); //TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init
FreshRSS_Context::init();
- if ($feedController->actualizeAction($simplePie) > 0) {
+ list($updated_feeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie);
+ if ($updated_feeds > 0) {
$nb++;
}
} catch (Exception $e) {
diff --git a/p/f.php b/p/f.php
index 0f23921e3..e4c82bb16 100644
--- a/p/f.php
+++ b/p/f.php
@@ -1,49 +1,9 @@
<?php
require('../constants.php');
-
-include(LIB_PATH . '/Favicon/Favicon.php');
-include(LIB_PATH . '/Favicon/DataAccess.php');
+require(LIB_PATH . '/favicons.php');
require(LIB_PATH . '/http-conditional.php');
-
-$favicons_dir = DATA_PATH . '/favicons/';
-$default_favicon = PUBLIC_PATH . '/themes/icons/default_favicon.ico';
-
-
-/* Télécharge le favicon d'un site et le place sur le serveur */
-function download_favicon($website, $dest) {
- global $favicons_dir, $default_favicon;
-
- $favicon_getter = new \Favicon\Favicon();
- $favicon_getter->setCacheDir($favicons_dir);
- $favicon_url = $favicon_getter->get($website);
-
- if ($favicon_url === false) {
- return @copy($default_favicon, $dest);
- }
-
- $c = curl_init($favicon_url);
- curl_setopt($c, CURLOPT_HEADER, false);
- curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($c, CURLOPT_BINARYTRANSFER, true);
- $img_raw = curl_exec($c);
- $status_code = curl_getinfo($c, CURLINFO_HTTP_CODE);
- curl_close($c);
-
- if ($status_code === 200) {
- $file = fopen($dest, 'w');
- if ($file !== false) {
- fwrite($file, $img_raw);
- fclose($file);
- return true;
- }
- }
-
- return false;
-}
-
-
function show_default_favicon($cacheSeconds = 3600) {
global $default_favicon;
@@ -69,23 +29,27 @@ $txt_mtime = @filemtime($txt);
header('Content-Type: image/x-icon');
-if ($ico_mtime == false || $txt_mtime > $ico_mtime) {
+if ($ico_mtime == false || $ico_mtime < $txt_mtime || ($ico_mtime < time() - (rand(15, 20) * 86400))) {
if ($txt_mtime == false) {
show_default_favicon(1800);
- return;
+ exit();
}
// no ico file or we should download a new one.
$url = file_get_contents($txt);
if (!download_favicon($url, $ico)) {
- // Download failed, show the default favicon
- show_default_favicon(86400);
- return;
+ // Download failed
+ if ($ico_mtime == false) {
+ show_default_favicon(86400);
+ exit();
+ } else {
+ touch($ico);
+ }
}
}
header('Content-Disposition: inline; filename="' . $id . '.ico"');
-if (!httpConditional($ico_mtime, 2592000, 2)) {
+if (!httpConditional($ico_mtime, rand(14, 21) * 86400, 2)) {
readfile($ico);
}
diff --git a/p/scripts/category.js b/p/scripts/category.js
index eb08ecd6e..fbcd83a01 100644
--- a/p/scripts/category.js
+++ b/p/scripts/category.js
@@ -95,6 +95,7 @@ function init_draggable() {
data : {
f_id: dragFeedId,
c_id: e.target.parentNode.getAttribute('data-cat-id'),
+ _csrf: context.csrf,
}
}).done(function() {
$(e.target).after(dragHtml);
diff --git a/p/scripts/global_view.js b/p/scripts/global_view.js
index f1b9c8ad4..de0b9cb9f 100644
--- a/p/scripts/global_view.js
+++ b/p/scripts/global_view.js
@@ -26,6 +26,7 @@ function load_panel(link) {
// Sans ça, si l'on scroll en lisant une catégorie par exemple,
// en en ouvrant une autre ensuite, on se retrouve au même point de scroll
$("#panel").scrollTop(0);
+ $(window).scrollTop(0);
$('#panel').on('click', '#nav_menu_read_all button, #bigMarkAsRead', function () {
console.log($(this).attr("formaction"));
diff --git a/p/scripts/install.js b/p/scripts/install.js
index d7f4e7b02..b7975fd6e 100644
--- a/p/scripts/install.js
+++ b/p/scripts/install.js
@@ -43,13 +43,15 @@ if (auth_type) {
function mySqlShowHide() {
var mysql = document.getElementById('mysql');
if (mysql) {
- mysql.style.display = document.getElementById('type').value === 'mysql' ? 'block' : 'none';
- if (document.getElementById('type').value !== 'mysql') {
+ if (document.getElementById('type').value === 'sqlite') {
document.getElementById('host').value = '';
document.getElementById('user').value = '';
document.getElementById('pass').value = '';
document.getElementById('base').value = '';
document.getElementById('prefix').value = '';
+ mysql.style.display = 'none';
+ } else {
+ mysql.style.display = 'block';
}
}
}
diff --git a/p/scripts/jquery.min.js b/p/scripts/jquery.min.js
index f6a6a99e6..4c5be4c0f 100644
--- a/p/scripts/jquery.min.js
+++ b/p/scripts/jquery.min.js
@@ -1,4 +1,4 @@
-/*! jQuery v3.1.0 | (c) jQuery Foundation | jquery.org/license */
-!function(a,b){"use strict";"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){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.0",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null!=a?a<0?this[a+this.length]:this[a]:f.call(this)},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.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||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},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?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;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;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=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=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"label"in b&&b.disabled===a||"form"in b&&b.disabled===a||"form"in b&&b.disabled===!1&&(b.isDisabled===a||b.isDisabled!==!a&&("label"in b||!ea(b))!==a)}}function pa(a){return ia(function(b){return b=+b,ia(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 qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.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]:[]}},d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(_,aa);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){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.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===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(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,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!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 ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.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},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.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=ga.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=ga.selectors={cacheLength:50,createPseudo:ia,match:V,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(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===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]||ga.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]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.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(_,aa).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("(^|"+K+")"+a+"("+K+"|$)"))&&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=ga.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(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},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,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(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:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).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:oa(!1),disabled:oa(!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 X.test(a.nodeName)},input:function(a){return W.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:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e)}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}}}function ua(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 va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(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?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(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}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.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(_,aa),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=V.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(_,aa),$.test(j[0].type)&&qa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&sa(j),!a)return G.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||$.test(a)&&qa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){if(r.isFunction(b))return r.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return r.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(C.test(b))return r.filter(b,a,c);b=r.filter(b,a)}return r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType})}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/\S+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,
-r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;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},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c||"false"!==c&&("null"===c?null:+c+""===c?+c:X.test(c)?JSON.parse(c):c)}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.hasData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.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=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),Z(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=Z(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.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 V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=V.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var $=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,_=new RegExp("^(?:([+-])=|)("+$+")([a-z%]*)$","i"),aa=["Top","Right","Bottom","Left"],ba=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},ca=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function da(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&_.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ea={};function fa(a){var b,c=a.ownerDocument,d=a.nodeName,e=ea[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),ea[d]=e,e)}function ga(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ba(d)&&(e[f]=fa(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ga(this,!0)},hide:function(){return ga(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ba(this)?r(this).show():r(this).hide()})}});var ha=/^(?:checkbox|radio)$/i,ia=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ja=/^$|\/(?:java|ecma)script/i,ka={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ka.optgroup=ka.option,ka.tbody=ka.tfoot=ka.colgroup=ka.caption=ka.thead,ka.th=ka.td;function la(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function ma(a,b){for(var c=0,d=a.length;c<d;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var na=/<|&#?\w+;/;function oa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(na.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ia.exec(f)||["",""])[1].toLowerCase(),i=ka[h]||ka._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=la(l.appendChild(f),"script"),j&&ma(g),c){k=0;while(f=g[k++])ja.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var pa=d.documentElement,qa=/^key/,ra=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,sa=/^([^.]*)(?:\.(.+)|)/;function ta(){return!0}function ua(){return!1}function va(){try{return d.activeElement}catch(a){}}function wa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)wa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ua;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(pa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),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),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==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,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;c<h;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?r(e,this).index(i)>-1:r.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==va()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===va()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&r.nodeName(this,"input"))return this.click(),!1},_default:function(a){return r.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ta:ua,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:ua,isPropagationStopped:ua,isImmediatePropagationStopped:ua,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ta,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ta,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ta,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&qa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ra.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return wa(this,a,b,c,d)},one:function(a,b,c,d){return wa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=ua),this.each(function(){r.event.remove(this,a,c,b)})}});var xa=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,ya=/<script|<style|<link/i,za=/checked\s*(?:[^=]|=\s*.checked.)/i,Aa=/^true\/(.*)/,Ba=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Ca(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Da(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ea(a){var b=Aa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ga(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ha.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ha(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&za.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(m&&(e=oa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(la(e,"script"),Da),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,la(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ea),l=0;l<i;l++)j=h[l],ja.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ba,""),k))}return a}function Ia(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(la(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&ma(la(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(xa,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=la(h),f=la(a),d=0,e=f.length;d<e;d++)Ga(f[d],g[d]);if(b)if(c)for(f=f||la(a),g=g||la(h),d=0,e=f.length;d<e;d++)Fa(f[d],g[d]);else Fa(a,h);return g=la(h,"script"),g.length>0&&ma(g,!i&&la(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.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 Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(la(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(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&&!ya.test(a)&&!ka[(ia.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(la(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(la(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ja=/^margin/,Ka=new RegExp("^("+$+")(?!px)[a-z%]+$","i"),La=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",pa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,pa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Ma(a,b,c){var d,e,f,g,h=a.style;return c=c||La(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ka.test(g)&&Ja.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Na(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Oa=/^(none|table(?!-c[ea]).+)/,Pa={position:"absolute",visibility:"hidden",display:"block"},Qa={letterSpacing:"0",fontWeight:"400"},Ra=["Webkit","Moz","ms"],Sa=d.createElement("div").style;function Ta(a){if(a in Sa)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ra.length;while(c--)if(a=Ra[c]+b,a in Sa)return a}function Ua(a,b,c){var d=_.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Va(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+aa[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+aa[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+aa[f]+"Width",!0,e))):(g+=r.css(a,"padding"+aa[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+aa[f]+"Width",!0,e)));return g}function Wa(a,b,c){var d,e=!0,f=La(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),d<=0||null==d){if(d=Ma(a,b,f),(d<0||null==d)&&(d=a.style[b]),Ka.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Va(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Ma(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ta(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=_.exec(c))&&e[1]&&(c=da(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ta(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Ma(a,b,d)),"normal"===e&&b in Qa&&(e=Qa[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Oa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Wa(a,b,d):ca(a,Pa,function(){return Wa(a,b,d)})},set:function(a,c,d){var e,f=d&&La(a),g=d&&Va(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=_.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ua(a,c,g)}}}),r.cssHooks.marginLeft=Na(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Ma(a,"marginLeft"))||a.getBoundingClientRect().left-ca(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+aa[d]+b]=f[d]||f[d-2]||f[0];return e}},Ja.test(a)||(r.cssHooks[a+b].set=Ua)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=La(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Xa(a,b,c,d,e){return new Xa.prototype.init(a,b,c,d,e)}r.Tween=Xa,Xa.prototype={constructor:Xa,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Xa.propHooks[this.prop];return a&&a.get?a.get(this):Xa.propHooks._default.get(this)},run:function(a){var b,c=Xa.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Xa.propHooks._default.set(this),this}},Xa.prototype.init.prototype=Xa.prototype,Xa.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Xa.propHooks.scrollTop=Xa.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Xa.prototype.init,r.fx.step={};var Ya,Za,$a=/^(?:toggle|show|hide)$/,_a=/queueHooks$/;function ab(){Za&&(a.requestAnimationFrame(ab),r.fx.tick())}function bb(){return a.setTimeout(function(){Ya=void 0}),Ya=r.now()}function cb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=aa[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function db(a,b,c){for(var d,e=(gb.tweeners[b]||[]).concat(gb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function eb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ba(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],$a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ga([a],!0),j=a.style.display||j,k=r.css(a,"display"),ga([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ga([a],!0),m.done(function(){p||ga([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=db(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function fb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function gb(a,b,c){var d,e,f=0,g=gb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Ya||bb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Ya||bb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(fb(k,j.opts.specialEasing);f<g;f++)if(d=gb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,db,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(gb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return da(c.elem,a,_.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;d<e;d++)c=a[d],gb.tweeners[c]=gb.tweeners[c]||[],gb.tweeners[c].unshift(b)},prefilters:[eb],prefilter:function(a,b){b?gb.prefilters.unshift(a):gb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:e.duration="number"==typeof e.duration?e.duration:e.duration in r.fx.speeds?r.fx.speeds[e.duration]:r.fx.speeds._default,null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ba).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=gb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&_a.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(cb(b,!0),a,d,e)}}),r.each({slideDown:cb("show"),slideUp:cb("hide"),slideToggle:cb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Ya=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),Ya=void 0},r.fx.timer=function(a){r.timers.push(a),a()?r.fx.start():r.timers.pop()},r.fx.interval=13,r.fx.start=function(){Za||(Za=a.requestAnimationFrame?a.requestAnimationFrame(ab):a.setInterval(r.fx.tick,r.fx.interval))},r.fx.stop=function(){a.cancelAnimationFrame?a.cancelAnimationFrame(Za):a.clearInterval(Za),Za=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var hb,ib=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return S(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?hb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);
-if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),hb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ib[b]||r.find.attr;ib[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ib[g],ib[g]=e,e=null!=c(a,b,d)?g:null,ib[g]=f),e}});var jb=/^(?:input|select|textarea|button)$/i,kb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):jb.test(a.nodeName)||kb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});var lb=/[\t\r\n\f]/g;function mb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,mb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,mb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,mb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=mb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(c)+" ").replace(lb," ").indexOf(b)>-1)return!0;return!1}});var nb=/\r/g,ob=/[\x20\t\r\n\f]+/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(nb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:r.trim(r.text(a)).replace(ob," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type,g=f?null:[],h=f?e+1:d.length,i=e<0?h:f?e:0;i<h;i++)if(c=d[i],(c.selected||i===e)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ha.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,""),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Qb=[],Rb=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Qb.pop()||r.expando+"_"+rb++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Rb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Rb.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Rb,"$1"+e):b.jsonp!==!1&&(b.url+=(sb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Qb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=B.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=oa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=r.trim(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length};function Sb(a){return r.isWindow(a)?a:9===a.nodeType&&a.defaultView}r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),d.width||d.height?(e=f.ownerDocument,c=Sb(e),b=e.documentElement,{top:d.top+c.pageYOffset-b.clientTop,left:d.left+c.pageXOffset-b.clientLeft}):d):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),r.nodeName(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||pa})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return S(this,function(a,d,e){var f=Sb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Na(o.pixelPosition,function(a,c){if(c)return c=Ma(a,b),Ka.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return S(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Tb=a.jQuery,Ub=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Ub),b&&a.jQuery===r&&(a.jQuery=Tb),r},b||(a.jQuery=a.$=r),r});
+/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */
+!function(a,b){"use strict";"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){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.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||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},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?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;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;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=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=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(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 qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),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){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.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===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(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,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!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 ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.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},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.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=ga.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=ga.selectors={cacheLength:50,createPseudo:ia,match:V,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(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===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]||ga.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]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.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(_,aa).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("(^|"+K+")"+a+"("+K+"|$)"))&&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=ga.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(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},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,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(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:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).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:oa(!1),disabled:oa(!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 X.test(a.nodeName)},input:function(a){return W.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:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(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 va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(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?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(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}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),
+a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;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},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:X.test(a)?JSON.parse(a):a)}function $(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=Z(c)}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.hasData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.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=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),$(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=$(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.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 V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=V.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var _=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,aa=new RegExp("^(?:([+-])=|)("+_+")([a-z%]*)$","i"),ba=["Top","Right","Bottom","Left"],ca=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},da=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function ea(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&aa.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var fa={};function ga(a){var b,c=a.ownerDocument,d=a.nodeName,e=fa[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),fa[d]=e,e)}function ha(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ca(d)&&(e[f]=ga(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ha(this,!0)},hide:function(){return ha(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ca(this)?r(this).show():r(this).hide()})}});var ia=/^(?:checkbox|radio)$/i,ja=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c<d;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var oa=/<|&#?\w+;/;function pa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(oa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ja.exec(f)||["",""])[1].toLowerCase(),i=la[h]||la._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),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),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==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,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==wa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===wa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&r.nodeName(this,"input"))return this.click(),!1},_default:function(a){return r.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ua:va,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:va,isPropagationStopped:va,isImmediatePropagationStopped:va,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ua,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ua,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ua,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&ra.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&sa.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return xa(this,a,b,c,d)},one:function(a,b,c,d){return xa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=va),this.each(function(){r.event.remove(this,a,c,b)})}});var ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/<script|<style|<link/i,Aa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ba=/^true\/(.*)/,Ca=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ha(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ia.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ia(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,ma(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Fa),l=0;l<i;l++)j=h[l],ka.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ca,""),k))}return a}function Ja(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(ma(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&na(ma(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(ya,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);if(b)if(c)for(f=f||ma(a),g=g||ma(h),d=0,e=f.length;d<e;d++)Ga(f[d],g[d]);else Ga(a,h);return g=ma(h,"script"),g.length>0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.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 Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(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&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(ma(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ia(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(ma(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ka=/^margin/,La=new RegExp("^("+_+")(?!px)[a-z%]+$","i"),Ma=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",qa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,qa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Na(a,b,c){var d,e,f,g,h=a.style;return c=c||Ma(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&La.test(g)&&Ka.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Oa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Pa=/^(none|table(?!-c[ea]).+)/,Qa={position:"absolute",visibility:"hidden",display:"block"},Ra={letterSpacing:"0",fontWeight:"400"},Sa=["Webkit","Moz","ms"],Ta=d.createElement("div").style;function Ua(a){if(a in Ta)return a;var b=a[0].toUpperCase()+a.slice(1),c=Sa.length;while(c--)if(a=Sa[c]+b,a in Ta)return a}function Va(a,b,c){var d=aa.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Wa(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ba[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ba[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ba[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ba[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ba[f]+"Width",!0,e)));return g}function Xa(a,b,c){var d,e=!0,f=Ma(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),d<=0||null==d){if(d=Na(a,b,f),(d<0||null==d)&&(d=a.style[b]),La.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Wa(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Na(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=aa.exec(c))&&e[1]&&(c=ea(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Na(a,b,d)),"normal"===e&&b in Ra&&(e=Ra[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Pa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Xa(a,b,d):da(a,Qa,function(){return Xa(a,b,d)})},set:function(a,c,d){var e,f=d&&Ma(a),g=d&&Wa(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=aa.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Va(a,c,g)}}}),r.cssHooks.marginLeft=Oa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Na(a,"marginLeft"))||a.getBoundingClientRect().left-da(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ba[d]+b]=f[d]||f[d-2]||f[0];return e}},Ka.test(a)||(r.cssHooks[a+b].set=Va)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=Ma(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function fb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ca(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],_a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ha([a],!0),j=a.style.display||j,k=r.css(a,"display"),ha([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ha([a],!0),m.done(function(){p||ha([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=eb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function gb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function hb(a,b,c){var d,e,f=0,g=hb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Za||cb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Za||cb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(gb(k,j.opts.specialEasing);f<g;f++)if(d=hb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,eb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(hb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return ea(c.elem,a,aa.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;d<e;d++)c=a[d],hb.tweeners[c]=hb.tweeners[c]||[],hb.tweeners[c].unshift(b)},prefilters:[fb],prefilter:function(a,b){b?hb.prefilters.unshift(a):hb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:"number"!=typeof e.duration&&(e.duration in r.fx.speeds?e.duration=r.fx.speeds[e.duration]:e.duration=r.fx.speeds._default),null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ca).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=hb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&ab.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(db(b,!0),a,d,e)}}),r.each({slideDown:db("show"),slideUp:db("hide"),slideToggle:db("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Za=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),Za=void 0},r.fx.timer=function(a){r.timers.push(a),a()?r.fx.start():r.timers.pop()},r.fx.interval=13,r.fx.start=function(){$a||($a=a.requestAnimationFrame?a.requestAnimationFrame(bb):a.setInterval(r.fx.tick,r.fx.interval))},r.fx.stop=function(){a.cancelAnimationFrame?a.cancelAnimationFrame($a):a.clearInterval($a),$a=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var ib,jb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return S(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)),
+void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Qb=[],Rb=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Qb.pop()||r.expando+"_"+rb++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Rb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Rb.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Rb,"$1"+e):b.jsonp!==!1&&(b.url+=(sb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Qb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=B.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=pa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=mb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length};function Sb(a){return r.isWindow(a)?a:9===a.nodeType&&a.defaultView}r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),d.width||d.height?(e=f.ownerDocument,c=Sb(e),b=e.documentElement,{top:d.top+c.pageYOffset-b.clientTop,left:d.left+c.pageXOffset-b.clientLeft}):d):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),r.nodeName(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||qa})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return S(this,function(a,d,e){var f=Sb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Oa(o.pixelPosition,function(a,c){if(c)return c=Na(a,b),La.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return S(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Tb=a.jQuery,Ub=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Ub),b&&a.jQuery===r&&(a.jQuery=Tb),r},b||(a.jQuery=a.$=r),r});
diff --git a/p/scripts/main.js b/p/scripts/main.js
index 8980fe2f6..078bec682 100644
--- a/p/scripts/main.js
+++ b/p/scripts/main.js
@@ -116,13 +116,8 @@ function incUnreadsFeed(article, feed_id, nb) {
var pending_entries = {};
function mark_read(active, only_not_read) {
- if (active.length === 0 ||
- (only_not_read === true && !active.hasClass("not_read"))) {
- return false;
- }
-
- var url = active.find("a.read").attr("href");
- if (url === undefined) {
+ if ((active.length === 0) || (!active.attr('id')) ||
+ (only_not_read && !active.hasClass("not_read"))) {
return false;
}
@@ -131,6 +126,9 @@ function mark_read(active, only_not_read) {
}
pending_entries[active.attr('id')] = true;
+ var url = '.?c=entry&a=read&id=' + active.attr('id').replace(/^flux_/, '') +
+ (active.hasClass('not_read') ? '' : '&is_read=0');
+
$.ajax({
type: 'POST',
url: url,
@@ -144,16 +142,18 @@ function mark_read(active, only_not_read) {
if (active.hasClass("not_read")) {
active.removeClass("not_read");
inc--;
- } else if (only_not_read !== true || active.hasClass("not_read")) {
+ } else {
active.addClass("not_read");
+ active.addClass("keep_unread");
inc++;
}
$r.find('.icon').replaceWith(data.icon);
- var feed_url = active.find(".website>a").attr("href"),
- feed_id = feed_url.substr(feed_url.lastIndexOf('f_'));
-
- incUnreadsFeed(active, feed_id, inc);
+ var feed_url = active.find(".website>a").attr("href");
+ if (feed_url) {
+ var feed_id = feed_url.substr(feed_url.lastIndexOf('f_'));
+ incUnreadsFeed(active, feed_id, inc);
+ }
faviconNbUnread();
delete pending_entries[active.attr('id')];
@@ -450,30 +450,32 @@ function auto_share(key) {
}
}
-function inMarkViewport(flux, box_to_follow) {
- var top = flux.offset().top;
- var height = flux.height(),
- begin = top + 3 * height / 4,
- bot = Math.min(begin + 75, top + height),
- windowTop = box_to_follow.scrollTop(),
- windowBot = windowTop + box_to_follow.height() / 2;
-
- return (windowBot >= begin && bot >= windowBot);
+function scrollAsRead(box_to_follow) {
+ var minTop = 40 + (context.current_view === 'global' ? box_to_follow.offset().top : box_to_follow.scrollTop());
+ $('.not_read:not(.keep_unread):visible').each(function () {
+ var $this = $(this);
+ if ($this.offset().top + $this.height() < minTop) {
+ mark_read($this, true);
+ }
+ });
}
function init_posts() {
- var box_to_follow = $(window);
- if (context.current_view === 'global') {
- box_to_follow = $("#panel");
- }
+ var box_to_follow = context.current_view === 'global' ? $("#panel") : $(window);
if (context.auto_mark_scroll) {
+ var lastScroll = 0, //Throttle
+ timerId = 0;
box_to_follow.scroll(function () {
- $('.not_read:visible').each(function () {
- if ($(this).children(".flux_content").is(':visible') && inMarkViewport($(this), box_to_follow)) {
- mark_read($(this), true);
- }
- });
+ window.clearTimeout(timerId);
+ if (lastScroll + 500 < Date.now()) {
+ lastScroll = Date.now();
+ scrollAsRead(box_to_follow);
+ } else {
+ timerId = window.setTimeout(function() {
+ scrollAsRead(box_to_follow);
+ }, 500);
+ }
});
}
@@ -752,7 +754,7 @@ function init_stream(divStream) {
});
divStream.on('click', '.flux .content a', function () {
- $(this).attr('target', '_blank');
+ $(this).attr('target', '_blank').attr('rel', 'noreferrer');
});
if (context.auto_mark_site) {
@@ -1021,14 +1023,13 @@ function focus_search() {
$('#search').focus();
}
+var freshrssLoadMoreEvent = document.createEvent('Event');
+freshrssLoadMoreEvent.initEvent('freshrss:load-more', true, true);
+
function init_load_more(box) {
box_load_more = box;
- if (!context.does_lazyload) {
- $('img[postpone], audio[postpone], iframe[postpone], video[postpone]').each(function () {
- this.removeAttribute('postpone');
- });
- }
+ document.body.dispatchEvent(freshrssLoadMoreEvent);
var $next_link = $("#load_more");
if (!$next_link.length) {
@@ -1125,6 +1126,7 @@ function init_confirm_action() {
return confirm(str_confirmation);
});
+ $('button.confirm').removeAttr('disabled');
}
function init_print_action() {
@@ -1154,10 +1156,10 @@ function init_share_observers() {
$('.share.add').on('click', function(e) {
var opt = $(this).siblings('select').find(':selected');
var row = $(this).parents('form').data(opt.data('form'));
- row = row.replace('##label##', opt.html().trim(), 'g');
- row = row.replace('##type##', opt.val(), 'g');
- row = row.replace('##help##', opt.data('help'), 'g');
- row = row.replace('##key##', shares, 'g');
+ row = row.replace(/##label##/g, opt.html().trim());
+ row = row.replace(/##type##/g, opt.val());
+ row = row.replace(/##help##/g, opt.data('help'));
+ row = row.replace(/##key##/g, shares);
$(this).parents('.form-group').before(row);
shares++;
diff --git a/p/themes/Pafat/pafat.css b/p/themes/Pafat/pafat.css
index 5e46a63ca..abe808eab 100644
--- a/p/themes/Pafat/pafat.css
+++ b/p/themes/Pafat/pafat.css
@@ -48,9 +48,6 @@ input, select, textarea {
vertical-align: middle;
}
-select{
- height:29px;
-}
option {
padding: 0 .5em;
}
diff --git a/p/themes/base-theme/template.css b/p/themes/base-theme/template.css
index 8a12423be..cc36c3ffa 100644
--- a/p/themes/base-theme/template.css
+++ b/p/themes/base-theme/template.css
@@ -44,6 +44,12 @@ p {
margin: 1em 0 0.5em;
font-size: 1em;
}
+sup {
+ line-height: 25px;
+ position: relative;
+ top: -0.8em;
+ vertical-align: baseline;
+}
/*=== Images */
img {
@@ -807,6 +813,10 @@ input:checked + .slide-container .properties {
display: none;
}
+.enclosure > [download] {
+ font-size: xx-large;
+ margin-left: .8em;
+}
/*=== MOBILE */
/*===========*/