diff --git a/README.md b/README.md index 25bb9d7..051e58a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,10 @@ FreshRSS for YunoHost * Site officiel : http://freshrss.org - +Changelog: +* Update to FreshRSS 1.0 +* Admin user no more mandatory +* Only the data directory belongs to www-data TODO: -* Finish Yunohost theme -* update scripts +* Finish Yunohost theme \ No newline at end of file diff --git a/conf/config.php b/conf/config.php index 79b3f5d..021f150 100644 --- a/conf/config.php +++ b/conf/config.php @@ -1,19 +1,11 @@ - array ( - 'environment' => 'production', - 'salt' => 'yunosalt', - 'base_url' => 'yunopath', - 'title' => 'FreshRSS', - 'default_user' => 'yunoadminuser', - 'allow_anonymous' => false, - 'allow_anonymous_refresh' => false, - 'auth_type' => 'http_auth', - 'api_enabled' => true, - 'unsafe_autologin_enabled' => false, - ), - 'db' => +return array ( + 'environment' => 'production', + 'salt' => 'yunosalt', + 'title' => 'FreshRSS', + 'default_user' => 'yunoadminuser', + 'auth_type' => 'http_auth', + 'db' => array ( 'type' => 'mysql', 'host' => 'localhost', @@ -22,4 +14,8 @@ 'base' => 'yunobase', 'prefix' => false, ), + 'allow_anonymous' => false, + 'allow_anonymous_refresh' => false, + 'unsafe_autologin_enabled' => false, + 'api_enabled' => true, ); \ No newline at end of file diff --git a/conf/dist_user.conf b/conf/dist_user.conf index 4e76155..87de38a 100644 --- a/conf/dist_user.conf +++ b/conf/dist_user.conf @@ -57,5 +57,6 @@ 'queries' => array ( ), + 'html5_notif_timeout' => 0, 'user' => 'YnoUser', ); \ No newline at end of file diff --git a/hooks/post_app_addaccess b/hooks/post_app_addaccess index bf78f88..f837cc8 100755 --- a/hooks/post_app_addaccess +++ b/hooks/post_app_addaccess @@ -31,11 +31,13 @@ then #remove temp sql sudo rm /tmp/$myuser-install.sql #copy default conf - sudo cp $app_path/data/user.php.dist $app_path/data/$myuser\_user.php + sudo mkdir $app_path/data/$myuser/ + sudo cp $app_path/data/user.php.dist $app_path/data/$myuser/config.php #change username - sudo sed -i "s/YnoUser/$myuser/g" $app_path/data/$myuser\_user.php + sudo sed -i "s/YnoUser/$myuser/g" $app_path/data/$myuser/config.php #add wallabag sharing - sudo sed -i "s@sharingArrayYnh@$sharingWallabag@g" $app_path/data/$myuser\_user.php - + sudo sed -i "s@sharingArrayYnh@$sharingWallabag@g" $app_path/data/$myuser/config.php + touch $app_path/data/$myuser/log.txt + chown www-data: $app_path/data/$myuser -R done fi \ No newline at end of file diff --git a/hooks/post_user_create b/hooks/post_user_create index 460eb1e..0af65dc 100755 --- a/hooks/post_user_create +++ b/hooks/post_user_create @@ -25,8 +25,12 @@ mysql -u $db_user -p$db_pwd $db_user < /tmp/$myuser-install.sql #remove temp sql sudo rm /tmp/$myuser-install.sql #copy default conf -sudo cp $app_path/data/user.php.dist $app_path/data/$myuser\_user.php +#copy default conf +sudo mkdir $app_path/data/$myuser/ +sudo cp $app_path/data/user.php.dist $app_path/data/$myuser/config.php #change username -sudo sed -i "s/YnoUser/$myuser/g" $app_path/data/$myuser\_user.php +sudo sed -i "s/YnoUser/$myuser/g" $app_path/data/$myuser/config.php #add wallabag sharing -sudo sed -i "s/sharingArrayYnh/$sharingWallabag/g" $final_path/data/$myuser\_user.php \ No newline at end of file +sudo sed -i "s/sharingArrayYnh/$sharingWallabag/g" $app_path/data/$myuser/config.php +touch $app_path/data/$myuser/log.txt +chown www-data: $app_path/data/$myuser -R \ No newline at end of file diff --git a/manifest.json b/manifest.json index 2f75497..8e661ab 100644 --- a/manifest.json +++ b/manifest.json @@ -32,8 +32,8 @@ { "name": "admin", "ask": { - "en": "Choose the default user (must be an existing YunoHost user)", - "fr": "Choisissez l'utilisateur par defaut (doit être un utiliser YunoHost existant)" + "en": "Choose the default user (leave empty if none)", + "fr": "Choisissez l'utilisateur par defaut (laissez vide si aucun)" }, "example": "homer" } diff --git a/scripts/install b/scripts/install index 15a8615..0c72c25 100755 --- a/scripts/install +++ b/scripts/install @@ -6,13 +6,15 @@ path=$2 admin_user=$3 -# Check user parameter -sudo yunohost user list --json | grep -qi "\"username\": \"$admin_user\"" -if [[ ! $? -eq 0 ]]; then - echo "Wrong user" - exit 1 +# Check user parameter if not empty +if [[ $admin_user -ne '' ]]; then + sudo yunohost user list --json | grep -qi "\"username\": \"$admin_user\"" + if [[ ! $? -eq 0 ]]; then + echo "Wrong user" + exit 1 + fi + sudo yunohost app setting freshrss admin_user -v $admin_user fi -sudo yunohost app setting freshrss admin_user -v $admin_user # Check domain/path availability sudo yunohost app checkurl $domain$path -a freshrss @@ -43,7 +45,11 @@ sudo sed -i "s/yunopass/$db_pwd/g" $final_path/data/config.php sudo sed -i "s/yunobase/$db_user/g" $final_path/data/config.php sudo sed -i "s/yunosalt/$app_salt/g" $final_path/data/config.php sudo sed -i "s@yunopath@$path@g" $final_path/data/config.php -sudo sed -i "s/yunoadminuser/$admin_user/g" $final_path/data/config.php +if [[ $admin_user -ne '' ]]; then + sudo sed -i "s/yunoadminuser/$admin_user/g" $final_path/data/config.php +else + sudo sed '/yunoadminuser/d' $final_path/data/config.php +fi # Add users @@ -66,11 +72,10 @@ do mysql -u $db_user -p$db_pwd $db_user < /tmp/$myuser-install.sql #remove temp sql sudo rm /tmp/$myuser-install.sql + sudo mkdir $final_path/data/$myuser/ #copy default conf - sudo cp ../conf/dist_user.conf $final_path/data/$myuser\_user.php - #change email - utilise seulement par persona - non necessaire pour ynh - #user_email=$(ldapsearch -h localhost -b uid=$myuser,ou=users,dc=yunohost,dc=org -x objectClass=mailAccount mail | grep mail: | head -n 1| sed 's/mail: //') - #sudo sed -i "s/ynoUserEmail/$user_email/g" $final_path/data/$myuser\_user.php + sudo cp ../conf/dist_user.conf $final_path/data/$myuser/config.php + touch $final_path/data/$myuser/log.txt #change username sudo sed -i "s/YnoUser/$myuser/g" $final_path/data/$myuser\_user.php sudo sed -i "s@sharingArrayYnh@$sharingWallabag@g" $final_path/data/$myuser\_user.php @@ -95,7 +100,7 @@ sudo mv /tmp/cronfreshrss /etc/cron.d/freshrss sudo chown root /etc/cron.d/freshrss # Set permissions to freshrss directory -sudo chown -R www-data: $final_path +sudo chown -R www-data: $final_path/data/ #skip api directory sudo yunohost app setting freshrss skipped_uris -v /api/greader.php diff --git a/scripts/update-ynh.php b/scripts/update-ynh.php new file mode 100644 index 0000000..e960583 --- /dev/null +++ b/scripts/update-ynh.php @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/scripts/upgrade b/scripts/upgrade index 1cf2f95..02cff7b 100755 --- a/scripts/upgrade +++ b/scripts/upgrade @@ -1,16 +1,15 @@ #!/bin/bash - #backup data folder final_path=/var/www/freshrss -sudo cp -a $final_path/data /tmp/freshrss-data.bak -#delete old folder -sudo rm -r $final_path -# Copy files to the right place -sudo mkdir -p $final_path -sudo cp -a ../sources/* $final_path -#remove data folder -sudo rm -r $final_path/data -#copy back data folder -sudo mv /tmp/freshrss-data.bak $final_path/data +# +if [[ -f $final_path/data/dist_user.conf ]]; then + rm $final_path/data/dist_user.conf +fi +#copy update script into freshrss path +cp update-ynh.php $final_path/ +#execute update +sudo php $final_path/update-ynh.php + +cp ../conf/dist_user.conf $final_path/data # Set permissions to freshrss directory -sudo chown -R www-data: $final_path +sudo chown -R www-data: $final_path/data diff --git a/sources/CHANGELOG b/sources/CHANGELOG index 1a4a887..b389f18 100755 --- a/sources/CHANGELOG +++ b/sources/CHANGELOG @@ -1,4 +1,90 @@ -# Journal des modifications +# Changelog + +## 2015-01-31 FreshRSS 1.0.0 / 1.1.0 (beta) + +* UI + * Slider math with Dark theme + * Add a message if request failed for mark as read / favourite +* I18n + * Fix some sentences + * Add German as a supported language + * Add some indications on password format +* Bug fixing + * Some shortcuts was never saved + * Global view didn't work if set by default + * Minz_Error was badly raised + * Feed update failed if nothing had changed (MySQL only) + * CRON task failed with multiple users + * Tricky bug caused by cookie path + * Email sharing was badly supported (no urlencode()) +* Misc. + * Add a CREDIT file with contributor names + * Update lib_opml + * Default favicon is now served by HTTP code 200 + * Change calls to syslog by Minz_Log::notice + * HTTP credentials are no longer logged + + +## 2015-01-15 FreshRSS 0.9.4 (beta) + +* Feature + * Extension system (!!): some extensions are available at https://github.com/FreshRSS/Extensions +* Refactoring + * Front controller (FreshRSS class) + * Configuration system + * Sharing system + * New data files organization +* Updates + * Remove restriction of 1h for updates + * Show the current version of FreshRSS and the next one +* UI + * Remove the "sticky position" of the feed aside (moved into an extension) + * "Show password" shows the password only while the user is pressing the mouse. + + +## 2014-12-12 FreshRSS 0.9.3 (beta) + +* SimplePie + * Support for content-type application/x-rss+xml + * New force_feed option (for feeds sent with the wrong content-type / MIME) by adding #force_feed at the end of the feed URL + * Improved error messages +* Statistics + * Add information on feed repartition pages + * Add percent repartition for the bigger feeds +* UI + * New theme selector + * Update Screwdriver theme + * Add BlueLagoon theme by Mister aiR +* Misc. + * Add option to remove articles after reading them + * Add comments + * Refactor i18n system to not load unnecessary strings + * Fix security issue in Minz_Error::error() method + * Fix redirection after refreshing a given feed + + +## 2014-10-31 FreshRSS 0.9.2 (beta) + +* UI + * New subscription page (introduce .box items) + * Change feed category by drag and drop + * New feed aside on the main page + * New configuration / administration organization +* Configuration + * New options in config.php for cache duration, timeout, max inactivity, max number of feeds and categories per user. +* Refactoring + * Refactor authentication system (introduce FreshRSS_Auth model) + * Refactor indexController (introduce FreshRSS_Context model) + * Use ```_t()```, ```_i()```, ```_url()```, ```Minz_Request::good()``` and ```Minz_Request::bad()``` as much as possible + * Refactor javascript_vars.phtml + * Better coding style +* I18n + * Introduce a new system for i18n keys (not finished yet) +* Misc. + * Fix global view (didn't work anymore) + * Add do_post_update for update system + * Introduce ```checkInstallAction``` to test if FreshRSS installation is ok + ## 2014-10-09 FreshRSS 0.8.1 / 0.9.1 (beta) diff --git a/sources/CREDITS b/sources/CREDITS new file mode 100755 index 0000000..47a968d --- /dev/null +++ b/sources/CREDITS @@ -0,0 +1,39 @@ +This is a credit file of people who have contributed to FreshRSS with, at least, +one commit on the FreshRSS repository (at https://github.com/FreshRSS/FreshRSS). +Please note a commit on THIS specific file is not considered as a contribution +(too easy!). It's purpose is to show even the smallest contribution is important. +People are sorted by name so please keep this order. + +--- + +Alexandre Alapetite +https://github.com/Alkarex + +Alexis Degrugillier +https://github.com/aledeg + +Alwaysin +https://github.com/Alwaysin + +Amaury Carrade +https://github.com/AmauryCarrade + +ealdraed +https://github.com/ealdraed + +Luc Didry +https://github.com/ldidry + +Marien Fressinaud +dev@marienfressinaud.fr +http://marienfressinaud.fr +https://github.com/marienfressinaud + +Nicolas Elie +https://github.com/nicolaselie + +plopoyop +https://github.com/plopoyop + +tomgue +https://github.com/tomgue diff --git a/sources/README.fr.md b/sources/README.fr.md index 2bc8872..380d7bc 100755 --- a/sources/README.fr.md +++ b/sources/README.fr.md @@ -9,26 +9,23 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ -* Développeur : Marien Fressinaud -* Version actuelle : 0.8.1 -* Date de publication : 2014-10-09 -* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) +* Licence : [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) # Note sur les branches **Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond : -* Utilisez [la branche master](https://github.com/marienfressinaud/FreshRSS/tree/master/) si vous visez la stabilité. -* [La branche beta](https://github.com/marienfressinaud/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois. -* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/marienfressinaud/FreshRSS/tree/dev) vous ouvre les bras ! +* Utilisez [la branche master](https://github.com/FreshRSS/FreshRSS/tree/master/) si vous visez la stabilité. +* [La branche beta](https://github.com/FreshRSS/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois. +* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras ! # Disclaimer Cette application a été développée pour s’adapter à des besoins personnels et non professionnels. Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement. Je m’engage néanmoins à répondre dans la mesure du possible aux demandes d’évolution si celles-ci me semblent justifiées. Privilégiez pour cela des demandes sur GitHub -(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail (dev@marienfressinaud.fr) +(https://github.com/FreshRSS/FreshRSS/issues). # Pré-requis * Serveur modeste, par exemple sous Linux ou Windows @@ -44,7 +41,7 @@ Privilégiez pour cela des demandes sur GitHub ![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png) # Installation -1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip) +1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip) 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 diff --git a/sources/README.md b/sources/README.md index 77fa9f9..a09a646 100755 --- a/sources/README.md +++ b/sources/README.md @@ -9,26 +9,23 @@ It is a multi-user application with an anonymous reading mode. * Official website: http://freshrss.org * Demo: http://demo.freshrss.org/ -* Developer: Marien Fressinaud -* Current version: 0.8.1 -* Publication date: 2014-10-09 -* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) +* License: [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) ![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) # Note on branches **This application is still in development!** Please use the branch that suits your needs: -* Use [the master branch](https://github.com/marienfressinaud/FreshRSS/tree/master/) if you need a stable version. -* [The beta branch](https://github.com/marienfressinaud/FreshRSS/tree/beta) is the default branch: new features are added on a monthly basis. -* For developers and tech savvy persons, [the dev branch](https://github.com/marienfressinaud/FreshRSS/tree/dev) is waiting for you! +* Use [the master branch](https://github.com/FreshRSS/FreshRSS/tree/master/) if you need a stable version. +* [The beta branch](https://github.com/FreshRSS/FreshRSS/tree/beta) is the default branch: new features are added on a monthly basis. +* For developers and tech savvy persons, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you! # Disclaimer This application was developed to fulfill personal needs not professional needs. There is no guarantee neither on its security nor its proper functioning. If there is feature requests which I think are good for the project, I'll do my best to include them. The best way is to open issues on GitHub -(https://github.com/marienfressinaud/FreshRSS/issues) or by email (dev@marienfressinaud.fr) +(https://github.com/FreshRSS/FreshRSS/issues). # Requirements * Light server running Linux or Windows @@ -37,14 +34,14 @@ The best way is to open issues on GitHub * PHP 5.2.1+ (PHP 5.3.7+ recommanded) * Required extensions: [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (only for API access on platforms under 64 bits) * Recommanded extensions : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip) -* MySQL 5.0.3+ (recommanded) ou SQLite 3.7.4+ +* MySQL 5.0.3+ (recommanded) or SQLite 3.7.4+ * A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ * Works on mobile ![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png) # Installation -1. Get FreshRSS with git or [by downloading the archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip) +1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip) 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 @@ -70,9 +67,9 @@ For example, if you want to run the script every hour: # Advices * For a better security, expose only the `./p/` folder on the web. - * Be aware that the `./data/` folder contain all personal data, so it is a bad idea to expose it. -* The `./constants.php` file define access to application folder. If you want to customize your installation, every thing happens here. -* If you encounter some problem, logs are accessibles from the interface or manually in `./data/log/*.log` files. + * Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it. +* The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here. +* If you encounter any problem, logs are accessibles from the interface or manually in `./data/log/*.log` files. # Backup * You need to keep `./data/config.php`, `./data/*_user.php` and `./data/persona/` files diff --git a/sources/app/Controllers/authController.php b/sources/app/Controllers/authController.php new file mode 100755 index 0000000..937c075 --- /dev/null +++ b/sources/app/Controllers/authController.php @@ -0,0 +1,349 @@ +token; + $token = Minz_Request::param('token', $current_token); + FreshRSS_Context::$user_conf->token = $token; + $ok &= FreshRSS_Context::$user_conf->save(); + + $anon = Minz_Request::param('anon_access', false); + $anon = ((bool)$anon) && ($anon !== 'no'); + $anon_refresh = Minz_Request::param('anon_refresh', false); + $anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no'); + $auth_type = Minz_Request::param('auth_type', 'none'); + $unsafe_autologin = Minz_Request::param('unsafe_autologin', false); + $api_enabled = Minz_Request::param('api_enabled', false); + if ($anon != FreshRSS_Context::$system_conf->allow_anonymous || + $auth_type != FreshRSS_Context::$system_conf->auth_type || + $anon_refresh != FreshRSS_Context::$system_conf->allow_anonymous_refresh || + $unsafe_autologin != FreshRSS_Context::$system_conf->unsafe_autologin_enabled || + $api_enabled != FreshRSS_Context::$system_conf->api_enabled) { + + // TODO: test values from form + FreshRSS_Context::$system_conf->auth_type = $auth_type; + FreshRSS_Context::$system_conf->allow_anonymous = $anon; + FreshRSS_Context::$system_conf->allow_anonymous_refresh = $anon_refresh; + FreshRSS_Context::$system_conf->unsafe_autologin_enabled = $unsafe_autologin; + FreshRSS_Context::$system_conf->api_enabled = $api_enabled; + + $ok &= FreshRSS_Context::$system_conf->save(); + } + + invalidateHttpCache(); + + if ($ok) { + Minz_Request::good(_t('feedback.conf.updated'), + array('c' => 'auth', 'a' => 'index')); + } else { + Minz_Request::bad(_t('feedback.conf.error'), + array('c' => 'auth', 'a' => 'index')); + } + } + } + + /** + * This action handles the login page. + * + * It forwards to the correct login page (form or Persona) or main page if + * the user is already connected. + */ + public function loginAction() { + if (FreshRSS_Auth::hasAccess()) { + Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } + + $auth_type = FreshRSS_Context::$system_conf->auth_type; + switch ($auth_type) { + case 'form': + Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin')); + break; + case 'persona': + Minz_Request::forward(array('c' => 'auth', 'a' => 'personaLogin')); + break; + case 'http_auth': + case 'none': + // It should not happened! + Minz_Error::error(404); + default: + // TODO load plugin instead + Minz_Error::error(404); + } + } + + /** + * This action handles form login page. + * + * If this action is reached through a POST request, username and password + * are compared to login the current user. + * + * Parameters are: + * - nonce (default: false) + * - username (default: '') + * - challenge (default: '') + * - keep_logged_in (default: false) + * + * @todo move unsafe autologin in an extension. + */ + public function formLoginAction() { + invalidateHttpCache(); + + $file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'); + Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime)); + + if (Minz_Request::isPost()) { + $nonce = Minz_Session::param('nonce'); + $username = Minz_Request::param('username', ''); + $challenge = Minz_Request::param('challenge', ''); + + $conf = get_user_configuration($username); + if (is_null($conf)) { + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'login')); + } + + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $conf->passwordHash, $nonce, $challenge + ); + if ($ok) { + // Set session parameter to give access to the user. + Minz_Session::_param('currentUser', $username); + Minz_Session::_param('passwordHash', $conf->passwordHash); + FreshRSS_Auth::giveAccess(); + + // Set cookie parameter if nedded. + if (Minz_Request::param('keep_logged_in')) { + FreshRSS_FormAuth::makeCookie($username, $conf->passwordHash); + } else { + FreshRSS_FormAuth::deleteCookie(); + } + + // All is good, go back to the index. + Minz_Request::good(_t('feedback.auth.login.success'), + array('c' => 'index', 'a' => 'index')); + } else { + Minz_Log::warning('Password mismatch for' . + ' user=' . $username . + ', nonce=' . $nonce . + ', c=' . $challenge); + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'login')); + } + } elseif (FreshRSS_Context::$system_conf->unsafe_autologin_enabled) { + $username = Minz_Request::param('u', ''); + $password = Minz_Request::param('p', ''); + Minz_Request::_param('p'); + + if (!$username) { + return; + } + + $conf = get_user_configuration($username); + if (is_null($conf)) { + return; + } + + if (!function_exists('password_verify')) { + include_once(LIB_PATH . '/password_compat.php'); + } + + $s = $conf->passwordHash; + $ok = password_verify($password, $s); + unset($password); + if ($ok) { + Minz_Session::_param('currentUser', $username); + Minz_Session::_param('passwordHash', $s); + FreshRSS_Auth::giveAccess(); + + Minz_Request::good(_t('feedback.auth.login.success'), + array('c' => 'index', 'a' => 'index')); + } else { + Minz_Log::warning('Unsafe password mismatch for user ' . $username); + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'login')); + } + } + } + + /** + * This action handles Persona login page. + * + * If this action is reached through a POST request, assertion from Persona + * is verificated and user connected if all is ok. + * + * Parameter is: + * - assertion (default: false) + * + * @todo: Persona system should be moved to a plugin + */ + public function personaLoginAction() { + $this->view->res = false; + + if (Minz_Request::isPost()) { + $this->view->_useLayout(false); + + $assert = Minz_Request::param('assertion'); + $url = 'https://verifier.login.persona.org/verify'; + $params = 'assertion=' . $assert . '&audience=' . + urlencode(Minz_Url::display(null, 'php', true)); + $ch = curl_init(); + $options = array( + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_POST => 2, + CURLOPT_POSTFIELDS => $params + ); + curl_setopt_array($ch, $options); + $result = curl_exec($ch); + curl_close($ch); + + $res = json_decode($result, true); + + $login_ok = false; + $reason = ''; + if ($res['status'] === 'okay') { + $email = filter_var($res['email'], FILTER_VALIDATE_EMAIL); + if ($email != '') { + $persona_file = DATA_PATH . '/persona/' . $email . '.txt'; + if (($current_user = @file_get_contents($persona_file)) !== false) { + $current_user = trim($current_user); + $conf = get_user_configuration($current_user); + if (!is_null($conf)) { + $login_ok = strcasecmp($email, $conf->mail_login) === 0; + } else { + $reason = 'Invalid configuration for user ' . + '[' . $current_user . ']'; + } + } + } else { + $reason = 'Invalid email format [' . $res['email'] . ']'; + } + } else { + $reason = $res['reason']; + } + + if ($login_ok) { + Minz_Session::_param('currentUser', $current_user); + Minz_Session::_param('mail', $email); + FreshRSS_Auth::giveAccess(); + invalidateHttpCache(); + } else { + Minz_Log::error($reason); + + $res = array(); + $res['status'] = 'failure'; + $res['reason'] = _t('feedback.auth.login.invalid'); + } + + header('Content-Type: application/json; charset=UTF-8'); + $this->view->res = $res; + } + } + + /** + * This action removes all accesses of the current user. + */ + public function logoutAction() { + invalidateHttpCache(); + FreshRSS_Auth::removeAccess(); + Minz_Request::good(_t('feedback.auth.logout.success'), + array('c' => 'index', 'a' => 'index')); + } + + /** + * This action resets the authentication system. + * + * After reseting, form auth is set by default. + */ + public function resetAction() { + Minz_View::prependTitle(_t('admin.auth.title_reset') . ' · '); + + Minz_View::appendScript(Minz_Url::display( + '/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js') + )); + + $this->view->no_form = false; + // Enable changement of auth only if Persona! + if (FreshRSS_Context::$system_conf->auth_type != 'persona') { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.auth.not_persona') + ); + $this->view->no_form = true; + return; + } + + $conf = get_user_configuration(FreshRSS_Context::$system_conf->default_user); + if (is_null($conf)) { + return; + } + + // Admin user must have set its master password. + if (!$conf->passwordHash) { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.auth.no_password_set') + ); + $this->view->no_form = true; + return; + } + + invalidateHttpCache(); + + if (Minz_Request::isPost()) { + $nonce = Minz_Session::param('nonce'); + $username = Minz_Request::param('username', ''); + $challenge = Minz_Request::param('challenge', ''); + + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $conf->passwordHash, $nonce, $challenge + ); + + if ($ok) { + FreshRSS_Context::$system_conf->auth_type = 'form'; + $ok = FreshRSS_Context::$system_conf->save(); + + if ($ok) { + Minz_Request::good(_t('feedback.auth.form.set')); + } else { + Minz_Request::bad(_t('feedback.auth.form.not_set'), + array('c' => 'auth', 'a' => 'reset')); + } + } else { + Minz_Log::warning('Password mismatch for' . + ' user=' . $username . + ', nonce=' . $nonce . + ', c=' . $challenge); + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'reset')); + } + } + } +} diff --git a/sources/app/Controllers/categoryController.php b/sources/app/Controllers/categoryController.php new file mode 100755 index 0000000..e65c146 --- /dev/null +++ b/sources/app/Controllers/categoryController.php @@ -0,0 +1,194 @@ +checkDefault(); + } + + /** + * This action creates a new category. + * + * Request parameter is: + * - new-category + */ + public function createAction() { + $catDAO = new FreshRSS_CategoryDAO(); + $url_redirect = array('c' => 'subscription', 'a' => 'index'); + + $limits = FreshRSS_Context::$system_conf->limits; + $this->view->categories = $catDAO->listCategories(false); + + if (count($this->view->categories) >= $limits['max_categories']) { + Minz_Request::bad(_t('feedback.sub.category.over_max', $limits['max_categories']), + $url_redirect); + } + + if (Minz_Request::isPost()) { + invalidateHttpCache(); + + $cat_name = Minz_Request::param('new-category'); + if (!$cat_name) { + Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect); + } + + $cat = new FreshRSS_Category($cat_name); + + if ($catDAO->searchByName($cat->name()) != null) { + Minz_Request::bad(_t('feedback.sub.category.name_exists'), $url_redirect); + } + + $values = array( + 'id' => $cat->id(), + 'name' => $cat->name(), + ); + + if ($catDAO->addCategory($values)) { + Minz_Request::good(_t('feedback.sub.category.created', $cat->name()), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action updates the given category. + * + * Request parameters are: + * - id + * - name + */ + public function updateAction() { + $catDAO = new FreshRSS_CategoryDAO(); + $url_redirect = array('c' => 'subscription', 'a' => 'index'); + + if (Minz_Request::isPost()) { + invalidateHttpCache(); + + $id = Minz_Request::param('id'); + $name = Minz_Request::param('name', ''); + if (strlen($name) <= 0) { + Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect); + } + + if ($catDAO->searchById($id) == null) { + Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect); + } + + $cat = new FreshRSS_Category($name); + $values = array( + 'name' => $cat->name(), + ); + + if ($catDAO->updateCategory($id, $values)) { + Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action deletes a category. + * Feeds in the given category are moved in the default category. + * Related user queries are deleted too. + * + * Request parameter is: + * - id (of a category) + */ + 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()) { + invalidateHttpCache(); + + $id = Minz_Request::param('id'); + if (!$id) { + Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect); + } + + if ($id === $default_category->id()) { + Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect); + } + + if ($feedDAO->changeCategory($id, $default_category->id()) === false) { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + + if ($catDAO->deleteCategory($id) === false) { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + + // Remove related queries. + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'c_' . $id, FreshRSS_Context::$user_conf->queries); + FreshRSS_Context::$user_conf->save(); + + Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect); + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action deletes all the feeds relative to a given category. + * Feed-related queries are deleted. + * + * Request parameter is: + * - id (of a category) + */ + public function emptyAction() { + $feedDAO = FreshRSS_Factory::createFeedDao(); + $url_redirect = array('c' => 'subscription', 'a' => 'index'); + + if (Minz_Request::isPost()) { + invalidateHttpCache(); + + $id = Minz_Request::param('id'); + if (!$id) { + Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect); + } + + // List feeds to remove then related user queries. + $feeds = $feedDAO->listByCategory($id); + + if ($feedDAO->deleteFeedByCategory($id)) { + // TODO: Delete old favicons + + // Remove related queries + foreach ($feeds as $feed) { + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $feed->id(), FreshRSS_Context::$user_conf->queries); + } + FreshRSS_Context::$user_conf->save(); + + Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } +} diff --git a/sources/app/Controllers/configureController.php b/sources/app/Controllers/configureController.php index 231865b..38ccd2b 100755 --- a/sources/app/Controllers/configureController.php +++ b/sources/app/Controllers/configureController.php @@ -8,175 +8,10 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * This action is called before every other action in that class. It is * the common boiler plate for every action. It is triggered by the * underlying framework. - * - * @todo see if the category default configuration is needed here or if - * we can move it to the categorize action */ public function firstAction() { - if (!$this->view->loginOk) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); - } - - $catDAO = new FreshRSS_CategoryDAO(); - $catDAO->checkDefault(); - } - - /** - * This action handles the category configuration page - * - * It displays the category configuration page. - * If this action is reached through a POST request, it loops through - * every category to check for modification then add a new category if - * needed then sends a notification to the user. - * If a category name is emptied, the category is deleted and all - * related feeds are moved to the default category. Related user queries - * are deleted too. - * If a category name is changed, it is updated. - */ - public function categorizeAction() { - $feedDAO = FreshRSS_Factory::createFeedDao(); - $catDAO = new FreshRSS_CategoryDAO(); - $defaultCategory = $catDAO->getDefault(); - $defaultId = $defaultCategory->id(); - - if (Minz_Request::isPost()) { - $cats = Minz_Request::param('categories', array()); - $ids = Minz_Request::param('ids', array()); - $newCat = trim(Minz_Request::param('new_category', '')); - - foreach ($cats as $key => $name) { - if (strlen($name) > 0) { - $cat = new FreshRSS_Category($name); - $values = array( - 'name' => $cat->name(), - ); - $catDAO->updateCategory($ids[$key], $values); - } elseif ($ids[$key] != $defaultId) { - $feedDAO->changeCategory($ids[$key], $defaultId); - $catDAO->deleteCategory($ids[$key]); - - // Remove related queries. - $this->view->conf->remove_query_by_get('c_' . $ids[$key]); - $this->view->conf->save(); - } - } - - if ($newCat != '') { - $cat = new FreshRSS_Category($newCat); - $values = array( - 'id' => $cat->id(), - 'name' => $cat->name(), - ); - - if ($catDAO->searchByName($newCat) == null) { - $catDAO->addCategory($values); - } - } - invalidateHttpCache(); - - Minz_Request::good(_t('categories_updated'), - array('c' => 'configure', 'a' => 'categorize')); - } - - $this->view->categories = $catDAO->listCategories(false); - $this->view->defaultCategory = $catDAO->getDefault(); - $this->view->feeds = $feedDAO->listFeeds(); - - Minz_View::prependTitle(_t('categories_management') . ' · '); - } - - /** - * This action handles the feed configuration page. - * - * It displays the feed configuration page. - * If this action is reached through a POST request, it stores all new - * configuraiton values then sends a notification to the user. - * - * The options available on the page are: - * - name - * - description - * - website URL - * - feed URL - * - category id (default: default category id) - * - CSS path to article on website - * - display in main stream (default: 0) - * - HTTP authentication - * - number of article to retain (default: -2) - * - refresh frequency (default: -2) - * Default values are empty strings unless specified. - */ - public function feedAction() { - $catDAO = new FreshRSS_CategoryDAO(); - $this->view->categories = $catDAO->listCategories(false); - - $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->view->feeds = $feedDAO->listFeeds(); - - $id = Minz_Request::param('id'); - if ($id == false && !empty($this->view->feeds)) { - $id = current($this->view->feeds)->id(); - } - - $this->view->flux = false; - if ($id != false) { - $this->view->flux = $this->view->feeds[$id]; - - if (!$this->view->flux) { - Minz_Error::error( - 404, - array('error' => array(_t('page_not_found'))) - ); - } else { - if (Minz_Request::isPost() && $this->view->flux) { - $user = Minz_Request::param('http_user', ''); - $pass = Minz_Request::param('http_pass', ''); - - $httpAuth = ''; - if ($user != '' || $pass != '') { - $httpAuth = $user . ':' . $pass; - } - - $cat = intval(Minz_Request::param('category', 0)); - - $values = array( - 'name' => Minz_Request::param('name', ''), - 'description' => sanitizeHTML(Minz_Request::param('description', '', true)), - 'website' => Minz_Request::param('website', ''), - 'url' => Minz_Request::param('url', ''), - 'category' => $cat, - 'pathEntries' => Minz_Request::param('path_entries', ''), - 'priority' => intval(Minz_Request::param('priority', 0)), - 'httpAuth' => $httpAuth, - 'keep_history' => intval(Minz_Request::param('keep_history', -2)), - 'ttl' => intval(Minz_Request::param('ttl', -2)), - ); - - if ($feedDAO->updateFeed($id, $values)) { - $this->view->flux->_category($cat); - $this->view->flux->faviconPrepare(); - $notif = array( - 'type' => 'good', - 'content' => _t('feed_updated') - ); - } else { - $notif = array( - 'type' => 'bad', - 'content' => _t('error_occurred_update') - ); - } - invalidateHttpCache(); - - Minz_Session::_param('notification', $notif); - Minz_Request::forward(array('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true); - } - - Minz_View::prependTitle(_t('rss_feed_management') . ' — ' . $this->view->flux->name() . ' · '); - } - } else { - Minz_View::prependTitle(_t('rss_feed_management') . ' · '); + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } } @@ -206,33 +41,33 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function displayAction() { if (Minz_Request::isPost()) { - $this->view->conf->_language(Minz_Request::param('language', 'en')); - $this->view->conf->_theme(Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme)); - $this->view->conf->_content_width(Minz_Request::param('content_width', 'thin')); - $this->view->conf->_topline_read(Minz_Request::param('topline_read', false)); - $this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false)); - $this->view->conf->_topline_date(Minz_Request::param('topline_date', false)); - $this->view->conf->_topline_link(Minz_Request::param('topline_link', false)); - $this->view->conf->_bottomline_read(Minz_Request::param('bottomline_read', false)); - $this->view->conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false)); - $this->view->conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false)); - $this->view->conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false)); - $this->view->conf->_bottomline_date(Minz_Request::param('bottomline_date', false)); - $this->view->conf->_bottomline_link(Minz_Request::param('bottomline_link', false)); - $this->view->conf->_html5_notif_timeout(Minz_Request::param('html5_notif_timeout', 0)); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en'); + FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme); + FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin'); + FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false); + FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false); + FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false); + FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false); + FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false); + FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false); + FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::param('bottomline_sharing', false); + FreshRSS_Context::$user_conf->bottomline_tags = Minz_Request::param('bottomline_tags', false); + FreshRSS_Context::$user_conf->bottomline_date = Minz_Request::param('bottomline_date', false); + FreshRSS_Context::$user_conf->bottomline_link = Minz_Request::param('bottomline_link', false); + FreshRSS_Context::$user_conf->html5_notif_timeout = Minz_Request::param('html5_notif_timeout', 0); + FreshRSS_Context::$user_conf->save(); - Minz_Session::_param('language', $this->view->conf->language); - Minz_Translate::reset(); + Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); + Minz_Translate::reset(FreshRSS_Context::$user_conf->language); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'display')); } $this->view->themes = FreshRSS_Themes::get(); - Minz_View::prependTitle(_t('display_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.display.title') . ' · '); } /** @@ -254,6 +89,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * - image lazy loading * - stick open articles to the top * - display a confirmation when reading all articles + * - auto remove article after reading * - article order (default: DESC) * - mark articles as read when: * - displayed @@ -264,35 +100,33 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function readingAction() { if (Minz_Request::isPost()) { - $this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); - $this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal')); - $this->view->conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL)); - $this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); - $this->view->conf->_display_posts(Minz_Request::param('display_posts', false)); - $this->view->conf->_display_categories(Minz_Request::param('display_categories', false)); - $this->view->conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false)); - $this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); - $this->view->conf->_lazyload(Minz_Request::param('lazyload', false)); - $this->view->conf->_sticky_post(Minz_Request::param('sticky_post', false)); - $this->view->conf->_reading_confirm(Minz_Request::param('reading_confirm', false)); - $this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); - $this->view->conf->_mark_when(array( + FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10); + FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal'); + FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive'); + FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false); + FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false); + FreshRSS_Context::$user_conf->display_categories = Minz_Request::param('display_categories', false); + FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false); + FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false); + FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false); + FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false); + FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false); + FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false); + FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC'); + FreshRSS_Context::$user_conf->mark_when = array( 'article' => Minz_Request::param('mark_open_article', false), 'site' => Minz_Request::param('mark_open_site', false), 'scroll' => Minz_Request::param('mark_scroll', false), 'reception' => Minz_Request::param('mark_upon_reception', false), - )); - $this->view->conf->save(); - - Minz_Session::_param('language', $this->view->conf->language); - Minz_Translate::reset(); + ); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'reading')); } - Minz_View::prependTitle(_t('reading_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.reading.title') . ' · '); } /** @@ -305,15 +139,15 @@ class FreshRSS_configure_Controller extends Minz_ActionController { public function sharingAction() { if (Minz_Request::isPost()) { $params = Minz_Request::params(); - $this->view->conf->_sharing($params['share']); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->sharing = $params['share']; + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'sharing')); } - Minz_View::prependTitle(_t('sharing') . ' · '); + Minz_View::prependTitle(_t('conf.sharing.title') . ' · '); } /** @@ -330,11 +164,11 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function shortcutAction() { $list_keys = array('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter', - 'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left', - 'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right', - 's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y', - 'z', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', - 'f10', 'f11', 'f12'); + 'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left', + 'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right', + 's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y', + 'z', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', + 'f10', 'f11', 'f12'); $this->view->list_keys = $list_keys; if (Minz_Request::isPost()) { @@ -347,24 +181,15 @@ class FreshRSS_configure_Controller extends Minz_ActionController { } } - $this->view->conf->_shortcuts($shortcuts_ok); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->shortcuts = $shortcuts_ok; + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('shortcuts_updated'), + Minz_Request::good(_t('feedback.conf.shortcuts_updated'), array('c' => 'configure', 'a' => 'shortcut')); } - Minz_View::prependTitle(_t('shortcuts') . ' · '); - } - - /** - * This action display the user configuration page - * - * @todo move that action in the user controller - */ - public function usersAction() { - Minz_View::prependTitle(_t('users') . ' · '); + Minz_View::prependTitle(_t('conf.shortcut.title') . ' · '); } /** @@ -384,23 +209,23 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function archivingAction() { if (Minz_Request::isPost()) { - $this->view->conf->_old_entries(Minz_Request::param('old_entries', 3)); - $this->view->conf->_keep_history_default(Minz_Request::param('keep_history_default', 0)); - $this->view->conf->_ttl_default(Minz_Request::param('ttl_default', -2)); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->old_entries = Minz_Request::param('old_entries', 3); + FreshRSS_Context::$user_conf->keep_history_default = Minz_Request::param('keep_history_default', 0); + FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', -2); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'archiving')); } - Minz_View::prependTitle(_t('archiving_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.archiving.title') . ' · '); $entryDAO = FreshRSS_Factory::createEntryDao(); $this->view->nb_total = $entryDAO->count(); $this->view->size_user = $entryDAO->size(); - if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { + if (FreshRSS_Auth::hasAccess('admin')) { $this->view->size_total = $entryDAO->size(true); } } @@ -421,19 +246,19 @@ class FreshRSS_configure_Controller extends Minz_ActionController { foreach ($queries as $key => $query) { if (!$query['name']) { - $query['name'] = _t('query_number', $key + 1); + $query['name'] = _t('conf.query.number', $key + 1); } } - $this->view->conf->_queries($queries); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->queries = $queries; + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'queries')); } else { $this->view->query_get = array(); $cat_dao = new FreshRSS_CategoryDAO(); $feed_dao = FreshRSS_Factory::createFeedDao(); - foreach ($this->view->conf->queries as $key => $query) { + foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { if (!isset($query['get'])) { continue; } @@ -489,7 +314,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { } } - Minz_View::prependTitle(_t('queries') . ' · '); + Minz_View::prependTitle(_t('conf.query.title') . ' · '); } /** @@ -501,22 +326,19 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function addQueryAction() { $whitelist = array('get', 'order', 'name', 'search', 'state'); - $queries = $this->view->conf->queries; + $queries = FreshRSS_Context::$user_conf->queries; $query = Minz_Request::params(); - $query['name'] = _t('query_number', count($queries) + 1); + $query['name'] = _t('conf.query.number', count($queries) + 1); foreach ($query as $key => $value) { if (!in_array($key, $whitelist)) { unset($query[$key]); } } - if (!empty($query['state']) && $query['state'] & FreshRSS_Entry::STATE_STRICT) { - $query['state'] -= FreshRSS_Entry::STATE_STRICT; - } $queries[] = $query; - $this->view->conf->_queries($queries); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->queries = $queries; + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('query_created', $query['name']), + Minz_Request::good(_t('feedback.conf.query_created', $query['name']), array('c' => 'configure', 'a' => 'queries')); } } diff --git a/sources/app/Controllers/entryController.php b/sources/app/Controllers/entryController.php index ab66d91..1d9989f 100755 --- a/sources/app/Controllers/entryController.php +++ b/sources/app/Controllers/entryController.php @@ -1,150 +1,181 @@ view->loginOk) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } - $this->params = array (); - $output = Minz_Request::param('output', ''); - if (($output != '') && ($this->view->conf->view_mode !== $output)) { - $this->params['output'] = $output; - } - - $this->redirect = false; - $ajax = Minz_Request::param ('ajax'); - if ($ajax) { - $this->view->_useLayout (false); + // If ajax request, we do not print layout + $this->ajax = Minz_Request::param('ajax'); + if ($this->ajax) { + $this->view->_useLayout(false); + Minz_Request::_param('ajax'); } } - public function lastAction () { - $ajax = Minz_Request::param ('ajax'); - if (!$ajax && $this->redirect) { - Minz_Request::forward (array ( - 'c' => 'index', - 'a' => 'index', - 'params' => $this->params - ), true); - } else { - Minz_Request::_param ('ajax'); - } - } - - public function readAction () { - $this->redirect = true; - - $id = Minz_Request::param ('id'); - $get = Minz_Request::param ('get'); - $nextGet = Minz_Request::param ('nextGet', $get); - $idMax = Minz_Request::param ('idMax', 0); + /** + * Mark one or several entries as read (or not!). + * + * If request concerns several entries, it MUST be a POST request. + * If request concerns several entries, only mark them as read is available. + * + * Parameters are: + * - id (default: false) + * - get (default: false) /(c_\d+|f_\d+|s|a)/ + * - nextGet (default: $get) + * - idMax (default: 0) + * - is_read (default: true) + */ + public function readAction() { + $id = Minz_Request::param('id'); + $get = Minz_Request::param('get'); + $next_get = Minz_Request::param('nextGet', $get); + $id_max = Minz_Request::param('idMax', 0); + $params = array(); $entryDAO = FreshRSS_Factory::createEntryDao(); - if ($id == false) { + if ($id === false) { + // id is false? It MUST be a POST request! if (!Minz_Request::isPost()) { return; } if (!$get) { - $entryDAO->markReadEntries ($idMax); + // No get? Mark all entries as read (from $id_max) + $entryDAO->markReadEntries($id_max); } else { - $typeGet = $get[0]; - $get = substr ($get, 2); - switch ($typeGet) { - case 'c': - $entryDAO->markReadCat ($get, $idMax); - break; - case 'f': - $entryDAO->markReadFeed ($get, $idMax); - break; - case 's': - $entryDAO->markReadEntries ($idMax, true); - break; - case 'a': - $entryDAO->markReadEntries ($idMax); - break; + $type_get = $get[0]; + $get = substr($get, 2); + switch($type_get) { + case 'c': + $entryDAO->markReadCat($get, $id_max); + break; + case 'f': + $entryDAO->markReadFeed($get, $id_max); + break; + case 's': + $entryDAO->markReadEntries($id_max, true); + break; + case 'a': + $entryDAO->markReadEntries($id_max); + break; } - if ($nextGet !== 'a') { - $this->params['get'] = $nextGet; + + if ($next_get !== 'a') { + // Redirect to the correct page (category, feed or starred) + // Not "a" because it is the default value if nothing is + // given. + $params['get'] = $next_get; } } - - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feeds_marked_read') - ); - Minz_Session::_param ('notification', $notif); } else { - $is_read = (bool)(Minz_Request::param ('is_read', true)); - $entryDAO->markRead ($id, $is_read); + $is_read = (bool)(Minz_Request::param('is_read', true)); + $entryDAO->markRead($id, $is_read); + } + + if (!$this->ajax) { + Minz_Request::good(_t('feedback.sub.feed.marked_read'), array( + 'c' => 'index', + 'a' => 'index', + 'params' => $params, + ), true); } } - public function bookmarkAction () { - $this->redirect = true; - - $id = Minz_Request::param ('id'); - if ($id) { + /** + * This action marks an entry as favourite (bookmark) or not. + * + * Parameter is: + * - id (default: false) + * - is_favorite (default: true) + * If id is false, nothing happened. + */ + public function bookmarkAction() { + $id = Minz_Request::param('id'); + $is_favourite = (bool)Minz_Request::param('is_favorite', true); + if ($id !== false) { $entryDAO = FreshRSS_Factory::createEntryDao(); - $entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true))); + $entryDAO->markFavorite($id, $is_favourite); + } + + if (!$this->ajax) { + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index', + ), true); } } + /** + * This action optimizes database to reduce its size. + * + * This action shouldbe reached by a POST request. + * + * @todo move this action in configure controller. + * @todo call this action through web-cron when available + */ public function optimizeAction() { - if (Minz_Request::isPost()) { - @set_time_limit(300); + $url_redirect = array( + 'c' => 'configure', + 'a' => 'archiving', + ); - // La table des entrées a tendance à grossir énormément - // Cette action permet d'optimiser cette table permettant de grapiller un peu de place - // Cette fonctionnalité n'est à appeler qu'occasionnellement - $entryDAO = FreshRSS_Factory::createEntryDao(); - $entryDAO->optimizeTable(); - - $feedDAO = FreshRSS_Factory::createFeedDao(); - $feedDAO->updateCachedValues(); - - invalidateHttpCache(); - - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('optimization_complete') - ); - Minz_Session::_param ('notification', $notif); + if (!Minz_Request::isPost()) { + Minz_Request::forward($url_redirect, true); } - Minz_Request::forward(array( - 'c' => 'configure', - 'a' => 'archiving' - ), true); + @set_time_limit(300); + + $entryDAO = FreshRSS_Factory::createEntryDao(); + $entryDAO->optimizeTable(); + + $feedDAO = FreshRSS_Factory::createFeedDao(); + $feedDAO->updateCachedValues(); + + invalidateHttpCache(); + Minz_Request::good(_t('feedback.admin.optimization_complete'), $url_redirect); } + /** + * This action purges old entries from feeds. + * + * @todo should be a POST request + * @todo should be in feedController + */ public function purgeAction() { @set_time_limit(300); - $nb_month_old = max($this->view->conf->old_entries, 1); + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); $feedDAO = FreshRSS_Factory::createFeedDao(); $feeds = $feedDAO->listFeeds(); - $nbTotal = 0; + $nb_total = 0; invalidateHttpCache(); foreach ($feeds as $feed) { - $feedHistory = $feed->keepHistory(); - if ($feedHistory == -2) { //default - $feedHistory = $this->view->conf->keep_history_default; + $feed_history = $feed->keepHistory(); + if ($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; } - if ($feedHistory >= 0) { - $nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feedHistory); + + if ($feed_history >= 0) { + $nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feed_history); if ($nb > 0) { - $nbTotal += $nb; - Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); - //$feedDAO->updateLastUpdate($feed->id()); + $nb_total += $nb; + Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']'); } } } @@ -152,16 +183,9 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $feedDAO->updateCachedValues(); invalidateHttpCache(); - - $notif = array( - 'type' => 'good', - 'content' => Minz_Translate::t('purge_completed', $nbTotal) - ); - Minz_Session::_param('notification', $notif); - - Minz_Request::forward(array( + Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array( 'c' => 'configure', 'a' => 'archiving' - ), true); + )); } } diff --git a/sources/app/Controllers/errorController.php b/sources/app/Controllers/errorController.php index 922650b..b0bafda 100755 --- a/sources/app/Controllers/errorController.php +++ b/sources/app/Controllers/errorController.php @@ -1,36 +1,51 @@ view->code = 'Error 403 - Forbidden'; - break; - case 404: - $this->view->code = 'Error 404 - Not found'; - break; - case 500: - $this->view->code = 'Error 500 - Internal Server Error'; - break; - case 503: - $this->view->code = 'Error 503 - Service Unavailable'; - break; - default: - $this->view->code = 'Error 404 - Not found'; + $code_int = Minz_Session::param('error_code', 404); + $error_logs = Minz_Session::param('error_logs', array()); + Minz_Session::_param('error_code'); + Minz_Session::_param('error_logs'); + + switch ($code_int) { + case 200 : + header('HTTP/1.1 200 OK'); + break; + case 403: + header('HTTP/1.1 403 Forbidden'); + $this->view->code = 'Error 403 - Forbidden'; + $this->view->errorMessage = _t('feedback.access.denied'); + break; + case 500: + header('HTTP/1.1 500 Internal Server Error'); + $this->view->code = 'Error 500 - Internal Server Error'; + break; + case 503: + header('HTTP/1.1 503 Service Unavailable'); + $this->view->code = 'Error 503 - Service Unavailable'; + break; + case 404: + default: + header('HTTP/1.1 404 Not Found'); + $this->view->code = 'Error 404 - Not found'; + $this->view->errorMessage = _t('feedback.access.not_found'); } - $errors = Minz_Request::param('logs', array()); - $this->view->errorMessage = trim(implode($errors)); - if ($this->view->errorMessage == '') { - switch(Minz_Request::param('code')) { - case 403: - $this->view->errorMessage = Minz_Translate::t('forbidden_access'); - break; - case 404: - default: - $this->view->errorMessage = Minz_Translate::t('page_not_found'); - break; - } + $error_message = trim(implode($error_logs)); + if ($error_message !== '') { + $this->view->errorMessage = $error_message; } Minz_View::prependTitle($this->view->code . ' · '); diff --git a/sources/app/Controllers/extensionController.php b/sources/app/Controllers/extensionController.php new file mode 100755 index 0000000..b6d2d3f --- /dev/null +++ b/sources/app/Controllers/extensionController.php @@ -0,0 +1,215 @@ +view->extension_list = array( + 'system' => array(), + 'user' => array(), + ); + + $extensions = Minz_ExtensionManager::listExtensions(); + foreach ($extensions as $ext) { + $this->view->extension_list[$ext->getType()][] = $ext; + } + } + + /** + * This action handles configuration of a given extension. + * + * Only administrator can configure a system extension. + * + * Parameters are: + * - e: the extension name (urlencoded) + * - additional parameters which should be handle by the extension + * handleConfigureAction() method (POST request). + */ + public function configureAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } else { + $this->indexAction(); + $this->view->change_view('extension', 'index'); + } + + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Error::error(404); + } + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $this->view->extension = $ext; + $this->view->extension->handleConfigureAction(); + } + + /** + * This action enables a disabled extension for the current user. + * + * System extensions can only be enabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function enableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if ($ext->isEnabled()) { + Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $res = $ext->install(); + + if ($res === true) { + $ext_list = $conf->extensions_enabled; + array_push_unique($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.enable.ok', $ext_name), + $url_redirect); + } else { + Minz_Log::warning('Can not enable extension ' . $ext_name . ': ' . $res); + Minz_Request::bad(_t('feedback.extensions.enable.ko', $ext_name, _url('index', 'logs')), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action disables an enabled extension for the current user. + * + * System extensions can only be disabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function disableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if (!$ext->isEnabled()) { + Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $res = $ext->uninstall(); + + if ($res === true) { + $ext_list = $conf->extensions_enabled; + array_remove($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.disable.ok', $ext_name), + $url_redirect); + } else { + Minz_Log::warning('Can not unable extension ' . $ext_name . ': ' . $res); + Minz_Request::bad(_t('feedback.extensions.disable.ko', $ext_name, _url('index', 'logs')), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action handles deletion of an extension. + * + * Only administrator can remove an extension. + * This action must be reached by a POST request. + * + * Parameter is: + * -e: extension name (urlencoded) + */ + public function removeAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + $res = recursive_unlink($ext->getPath()); + if ($res) { + Minz_Request::good(_t('feedback.extensions.removed', $ext_name), + $url_redirect); + } else { + Minz_Request::bad(_t('feedback.extensions.cannot_delete', $ext_name), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } +} diff --git a/sources/app/Controllers/feedController.php b/sources/app/Controllers/feedController.php index f75c969..6f544d8 100755 --- a/sources/app/Controllers/feedController.php +++ b/sources/app/Controllers/feedController.php @@ -1,180 +1,217 @@ view->loginOk) { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { // Token is useful in the case that anonymous refresh is forbidden // and CRON task cannot be used with php command so the user can // set a CRON task to refresh his feeds by using token inside url - $token = $this->view->conf->token; - $token_param = Minz_Request::param ('token', ''); + $token = FreshRSS_Context::$user_conf->token; + $token_param = Minz_Request::param('token', ''); $token_is_ok = ($token != '' && $token == $token_param); - $action = Minz_Request::actionName (); - if (!(($token_is_ok || Minz_Configuration::allowAnonymousRefresh()) && - $action === 'actualize') - ) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); + $action = Minz_Request::actionName(); + $allow_anonymous_refresh = FreshRSS_Context::$system_conf->allow_anonymous_refresh; + if ($action !== 'actualize' || + !($allow_anonymous_refresh || $token_is_ok)) { + Minz_Error::error(403); } } } - public function addAction () { - $url = Minz_Request::param('url_rss', false); + /** + * This action subscribes to a feed. + * + * It can be reached by both GET and POST requests. + * + * GET request displays a form to add and configure a feed. + * Request parameter is: + * - url_rss (default: false) + * + * POST request adds a feed in database. + * Parameters are: + * - url_rss (default: false) + * - category (default: false) + * - new_category (required if category == 'nc') + * - http_user (default: false) + * - http_pass (default: false) + * It tries to get website information from RSS feed. + * If no category is given, feed is added to the default one. + * + * If url_rss is false, nothing happened. + */ + public function addAction() { + $url = Minz_Request::param('url_rss'); if ($url === false) { + // No url, do nothing Minz_Request::forward(array( - 'c' => 'configure', - 'a' => 'feed' + 'c' => 'subscription', + 'a' => 'index' ), true); } $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->catDAO = new FreshRSS_CategoryDAO (); - $this->catDAO->checkDefault (); + $this->catDAO = new FreshRSS_CategoryDAO(); + $url_redirect = array( + 'c' => 'subscription', + 'a' => 'index', + 'params' => array(), + ); + + $limits = FreshRSS_Context::$system_conf->limits; + $this->view->feeds = $feedDAO->listFeeds(); + if (count($this->view->feeds) >= $limits['max_feeds']) { + Minz_Request::bad(_t('feedback.sub.feed.over_max', $limits['max_feeds']), + $url_redirect); + } if (Minz_Request::isPost()) { @set_time_limit(300); - - $cat = Minz_Request::param ('category', false); + $cat = Minz_Request::param('category'); if ($cat === 'nc') { - $new_cat = Minz_Request::param ('new_category'); + // 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) { - $def_cat = $this->catDAO->getDefault (); - $cat = $def_cat->id (); + // 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(); } - $user = Minz_Request::param ('http_user'); - $pass = Minz_Request::param ('http_pass'); - $params = array (); + // HTTP information are useful if feed is protected behind a + // HTTP authentication + $user = Minz_Request::param('http_user'); + $pass = Minz_Request::param('http_pass'); + $http_auth = ''; + if ($user != '' || $pass != '') { + $http_auth = $user . ':' . $pass; + } - $transactionStarted = false; + $transaction_started = false; try { - $feed = new FreshRSS_Feed ($url); - $feed->_category ($cat); - - $httpAuth = ''; - if ($user != '' || $pass != '') { - $httpAuth = $user . ':' . $pass; - } - $feed->_httpAuth ($httpAuth); - - $feed->load(true); - - $values = array ( - 'url' => $feed->url (), - 'category' => $feed->category (), - 'name' => $feed->name (), - 'website' => $feed->website (), - 'description' => $feed->description (), - 'lastUpdate' => time (), - 'httpAuth' => $feed->httpAuth (), - ); - - if ($feedDAO->searchByUrl ($values['url'])) { - // on est déjà abonné à ce flux - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('already_subscribed', $feed->name ()) - ); - Minz_Session::_param ('notification', $notif); - } else { - $id = $feedDAO->addFeed ($values); - if (!$id) { - // problème au niveau de la base de données - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('feed_not_added', $feed->name ()) - ); - Minz_Session::_param ('notification', $notif); - } else { - $feed->_id ($id); - $feed->faviconPrepare(); - - $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; - - $entryDAO = FreshRSS_Factory::createEntryDao(); - $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order - - // on calcule la date des articles les plus anciens qu'on accepte - $nb_month_old = $this->view->conf->old_entries; - $date_min = time () - (3600 * 24 * 30 * $nb_month_old); - - //MySQL: http://docs.oracle.com/cd/E17952_01/refman-5.5-en/optimizing-innodb-transaction-management.html - //SQLite: http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite - $preparedStatement = $entryDAO->addEntryPrepare(); - $transactionStarted = true; - $feedDAO->beginTransaction(); - // on ajoute les articles en masse sans vérification - foreach ($entries as $entry) { - $values = $entry->toArray(); - $values['id_feed'] = $feed->id(); - $values['id'] = min(time(), $entry->date(true)) . uSecString(); - $values['is_read'] = $is_read; - $entryDAO->addEntry($values, $preparedStatement); - } - $feedDAO->updateLastUpdate($feed->id()); - if ($transactionStarted) { - $feedDAO->commit(); - } - $transactionStarted = false; - - // ok, ajout terminé - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feed_added', $feed->name ()) - ); - Minz_Session::_param ('notification', $notif); - - // permet de rediriger vers la page de conf du flux - $params['id'] = $feed->id (); - } - } + $feed = new FreshRSS_Feed($url); } catch (FreshRSS_BadUrl_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('invalid_url', $url) - ); - Minz_Session::_param ('notification', $notif); + // Given url was not a valid url! + Minz_Log::warning($e->getMessage()); + Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect); + } + + try { + $feed->load(true); } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) + // 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_Session::_param ('notification', $notif); } catch (Minz_FileNotExistException $e) { - // Répertoire de cache n'existe pas - Minz_Log::record ($e->getMessage (), Minz_Log::ERROR); - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) + // Cache directory doesn't exist! + Minz_Log::error($e->getMessage()); + Minz_Request::bad( + _t('feedback.sub.feed.internal_problem', _url('index', 'logs')), + $url_redirect ); - Minz_Session::_param ('notification', $notif); - } - if ($transactionStarted) { - $feedDAO->rollBack (); } - Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true); + if ($feedDAO->searchByUrl($feed->url())) { + Minz_Request::bad( + _t('feedback.sub.feed.already_subscribed', $feed->name()), + $url_redirect + ); + } + + $feed->_category($cat); + $feed->_httpAuth($http_auth); + + // Call the extension hook + $name = $feed->name(); + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (is_null($feed)) { + Minz_Request::bad(_t('feed_not_added', $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(); + + $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. + $prepared_statement = $entryDAO->addEntryPrepare(); + $feedDAO->beginTransaction(); + foreach ($entries as $entry) { + // Entries are added without any verification. + $entry->_feed($feed->id()); + $entry->_id(min(time(), $entry->date(true)) . uSecString()); + $entry->_isRead($is_read); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + + $values = $entry->toArray(); + $entryDAO->addEntry($values, $prepared_statement); + } + $feedDAO->updateLastUpdate($feed->id()); + $feedDAO->commit(); + + // Entries are in DB, we redirect to feed configuration page. + $url_redirect['params']['id'] = $feed->id(); + Minz_Request::good(_t('feedback.sub.feed.added', $feed->name()), $url_redirect); } else { + // GET request: we must ask confirmation to user before adding feed. + Minz_View::prependTitle(_t('sub.feed.title_add') . ' · '); - // GET request so we must ask confirmation to user - Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · '); $this->view->categories = $this->catDAO->listCategories(false); $this->view->feed = new FreshRSS_Feed($url); try { - // We try to get some more information about the feed + // We try to get more information about the feed. $this->view->feed->load(true); $this->view->load_ok = true; } catch (Exception $e) { @@ -183,256 +220,291 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed = $feedDAO->searchByUrl($this->view->feed->url()); if ($feed) { - // Already subscribe so we redirect to the feed configuration page - $notif = array( - 'type' => 'bad', - 'content' => Minz_Translate::t( - 'already_subscribed', $feed->name() - ) - ); - Minz_Session::_param('notification', $notif); - - Minz_Request::forward(array( - 'c' => 'configure', - 'a' => 'feed', - 'params' => array( - 'id' => $feed->id() - ) - ), true); + // Already subscribe so we redirect to the feed configuration page. + $url_redirect['params']['id'] = $feed->id(); + Minz_Request::good(_t('feedback.sub.feed.already_subscribed', $feed->name()), $url_redirect); } } } - public function truncateAction () { - if (Minz_Request::isPost ()) { - $id = Minz_Request::param ('id'); - $feedDAO = FreshRSS_Factory::createFeedDao(); - $n = $feedDAO->truncate($id); - $notif = array( - 'type' => $n === false ? 'bad' : 'good', - 'content' => Minz_Translate::t ('n_entries_deleted', $n) - ); - Minz_Session::_param ('notification', $notif); - invalidateHttpCache(); - Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true); + /** + * This action remove entries from a given feed. + * + * It should be reached by a POST action. + * + * Parameter is: + * - id (default: false) + */ + public function truncateAction() { + $id = Minz_Request::param('id'); + $url_redirect = array( + 'c' => 'subscription', + 'a' => 'index', + 'params' => array('id' => $id) + ); + + if (!Minz_Request::isPost()) { + Minz_Request::forward($url_redirect, true); + } + + $feedDAO = FreshRSS_Factory::createFeedDao(); + $n = $feedDAO->truncate($id); + + invalidateHttpCache(); + if ($n === false) { + Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect); + } else { + Minz_Request::good(_t('feedback.sub.feed.n_entries_deleted', $n), $url_redirect); } } - public function actualizeAction () { + /** + * This action actualizes entries from one or several feeds. + * + * Parameters are: + * - id (default: false) + * - force (default: false) + * If id is 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() { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); $entryDAO = FreshRSS_Factory::createEntryDao(); Minz_Session::_param('actualize_feeds', false); - $id = Minz_Request::param ('id'); - $force = Minz_Request::param ('force', false); + $id = Minz_Request::param('id'); + $force = Minz_Request::param('force'); - // on créé la liste des flux à mettre à actualiser - // si on veut mettre un flux à jour spécifiquement, on le met - // dans la liste, mais seul (permet d'automatiser le traitement) - $feeds = array (); + // Create a list of feeds to actualize. + // If id is set and valid, corresponding feed is added to the list but + // alone in order to automatize further process. + $feeds = array(); if ($id) { - $feed = $feedDAO->searchById ($id); + $feed = $feedDAO->searchById($id); if ($feed) { - $feeds = array ($feed); + $feeds[] = $feed; } } else { - $feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default); + $feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } - // on calcule la date des articles les plus anciens qu'on accepte - $nb_month_old = max($this->view->conf->old_entries, 1); - $date_min = time () - (3600 * 24 * 30 * $nb_month_old); + // Calculate date of oldest entries we accept in DB. + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); + $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - $i = 0; - $flux_update = 0; - $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; + $updated_feeds = 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { if (!$feed->lock()) { - Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE); + Minz_Log::notice('Feed already being actualized: ' . $feed->url()); continue; } + try { - $url = $feed->url(); - $feedHistory = $feed->keepHistory(); - + // Load entries $feed->load(false); - $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order - $hasTransaction = false; - - if (count($entries) > 0) { - //For this feed, check last n entry GUIDs already in database - $existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1); - $useDeclaredDate = empty($existingGuids); - - if ($feedHistory == -2) { //default - $feedHistory = $this->view->conf->keep_history_default; - } - - $preparedStatement = $entryDAO->addEntryPrepare(); - $hasTransaction = true; - $feedDAO->beginTransaction(); - - // On ne vérifie pas strictement que l'article n'est pas déjà en BDD - // La BDD refusera l'ajout car (id_feed, guid) doit être unique - foreach ($entries as $entry) { - $eDate = $entry->date(true); - if ((!isset($existingGuids[$entry->guid()])) && - (($feedHistory != 0) || ($eDate >= $date_min))) { - $values = $entry->toArray(); - //Use declared date at first import, otherwise use discovery date - $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? - min(time(), $eDate) . uSecString() : - uTimeString(); - $values['is_read'] = $is_read; - $entryDAO->addEntry($values, $preparedStatement); - } - } - } - - if (($feedHistory >= 0) && (rand(0, 30) === 1)) { - if (!$hasTransaction) { - $feedDAO->beginTransaction(); - } - $nb = $feedDAO->cleanOldEntries ($feed->id (), $date_min, max($feedHistory, count($entries) + 10)); - if ($nb > 0) { - Minz_Log::record ($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); - } - } - - // on indique que le flux vient d'être mis à jour en BDD - $feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction); - if ($hasTransaction) { - $feedDAO->commit(); - } - $flux_update++; - if (($feed->url() !== $url)) { //HTTP 301 Moved Permanently - Minz_Log::record('Feed ' . $url . ' moved permanently to ' . $feed->url(), Minz_Log::NOTICE); - $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); - } } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE); - $feedDAO->updateLastUpdate ($feed->id (), 1); + Minz_Log::notice($e->getMessage()); + $feedDAO->updateLastUpdate($feed->id(), 1); + $feed->unlock(); + continue; + } + + $url = $feed->url(); + $feed_history = $feed->keepHistory(); + if ($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; + } + + // We want chronological order and SimplePie uses reverse order. + $entries = array_reverse($feed->entries()); + if (count($entries) > 0) { + // For this feed, check last n entry GUIDs already in database. + $existing_guids = array_fill_keys($entryDAO->listLastGuidsByFeed( + $feed->id(), count($entries) + 10 + ), 1); + $use_declared_date = empty($existing_guids); + + // Add entries in database if possible. + $prepared_statement = $entryDAO->addEntryPrepare(); + $feedDAO->beginTransaction(); + foreach ($entries as $entry) { + $entry_date = $entry->date(true); + if (isset($existing_guids[$entry->guid()]) || + ($feed_history == 0 && $entry_date < $date_min)) { + // This entry already exists in DB or should not be added + // considering configuration and date. + continue; + } + + $id = uTimeString(); + if ($use_declared_date || $entry_date < $date_min) { + // Use declared date at first import. + $id = min(time(), $entry_date) . uSecString(); + } + + $entry->_id($id); + $entry->_isRead($is_read); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + + $values = $entry->toArray(); + $entryDAO->addEntry($values, $prepared_statement); + } + } + + if ($feed_history >= 0 && rand(0, 30) === 1) { + // TODO: move this function in web cron when available (see entry::purge) + // Remove old entries once in 30. + if (!$feedDAO->hasTransaction()) { + $feedDAO->beginTransaction(); + } + + $nb = $feedDAO->cleanOldEntries($feed->id(), + $date_min, + max($feed_history, count($entries) + 10)); + if ($nb > 0) { + Minz_Log::debug($nb . ' old entries cleaned in feed [' . + $feed->url() . ']'); + } + } + + $feedDAO->updateLastUpdate($feed->id(), 0, $feedDAO->hasTransaction()); + if ($feedDAO->hasTransaction()) { + $feedDAO->commit(); + } + + if ($feed->url() !== $url) { + // HTTP 301 Moved Permanently + Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url()); + $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } $feed->faviconPrepare(); $feed->unlock(); + $updated_feeds++; unset($feed); - // On arrête à 10 flux pour ne pas surcharger le serveur - // sauf si le paramètre $force est à vrai - $i++; - if ($i >= 10 && !$force) { + // No more than 10 feeds unless $force is true to avoid overloading + // the server. + if ($updated_feeds >= 10 && !$force) { break; } } - $url = array (); - if ($flux_update === 1) { - // on a mis un seul flux à jour - $feed = reset ($feeds); - $notif = array ( + if (Minz_Request::param('ajax')) { + // Most of the time, ajax request is for only one feed. But since + // there are several parallel requests, we should return that there + // are several updated feeds. + $notif = array( 'type' => 'good', - 'content' => Minz_Translate::t ('feed_actualized', $feed->name ()) - ); - } elseif ($flux_update > 1) { - // plusieurs flux on été mis à jour - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('n_feeds_actualized', $flux_update) - ); - } else { - // aucun flux n'a été mis à jour, oups - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('no_feed_to_refresh') + 'content' => _t('feedback.sub.feed.actualizeds') ); + Minz_Session::_param('notification', $notif); + // No layout in ajax request. + $this->view->_useLayout(false); + return; } - if ($i === 1) { - // Si on a voulu mettre à jour qu'un flux - // on filtre l'affichage par ce flux - $feed = reset ($feeds); - $url['params'] = array ('get' => 'f_' . $feed->id ()); - } - - if (Minz_Request::param ('ajax', 0) === 0) { - Minz_Session::_param ('notification', $notif); - Minz_Request::forward ($url, true); + // Redirect to the main page with correct notification. + if ($updated_feeds === 1) { + $feed = reset($feeds); + Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array( + 'params' => array('get' => 'f_' . $feed->id()) + )); + } elseif ($updated_feeds > 1) { + Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array()); } else { - // Une requête Ajax met un seul flux à jour. - // Comme en principe plusieurs requêtes ont lieu, - // on indique que "plusieurs flux ont été mis à jour". - // Cela permet d'avoir une notification plus proche du - // ressenti utilisateur - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feeds_actualized') - ); - Minz_Session::_param ('notification', $notif); - // et on désactive le layout car ne sert à rien - $this->view->_useLayout (false); + Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array()); } } - public function deleteAction () { - if (Minz_Request::isPost ()) { - $type = Minz_Request::param ('type', 'feed'); - $id = Minz_Request::param ('id'); + /** + * This action changes the category of a feed. + * + * This page must be reached by a POST request. + * + * Parameters are: + * - f_id (default: false) + * - c_id (default: false) + * If c_id is false, default category is used. + * + * @todo should handle order of the feed inside the category. + */ + public function moveAction() { + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'subscription'), true); + } - $feedDAO = FreshRSS_Factory::createFeedDao(); - if ($type == 'category') { - // List feeds to remove then related user queries. - $feeds = $feedDAO->listByCategory($id); + $feed_id = Minz_Request::param('f_id'); + $cat_id = Minz_Request::param('c_id'); - if ($feedDAO->deleteFeedByCategory ($id)) { - // Remove related queries - foreach ($feeds as $feed) { - $this->view->conf->remove_query_by_get('f_' . $feed->id()); - } - $this->view->conf->save(); + 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(); + } - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('category_emptied') - ); - //TODO: Delete old favicons - } else { - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('error_occured') - ); - } - } else { - if ($feedDAO->deleteFeed ($id)) { - // Remove related queries - $this->view->conf->remove_query_by_get('f_' . $id); - $this->view->conf->save(); + $feedDAO = FreshRSS_Factory::createFeedDao(); + $values = array('category' => $cat_id); - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feed_deleted') - ); - //TODO: Delete old favicon - } else { - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('error_occured') - ); - } - } + $feed = $feedDAO->searchById($feed_id); + if ($feed && ($feed->category() == $cat_id || + $feedDAO->updateFeed($feed_id, $values))) { + // TODO: return something useful + } else { + Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' . + 'in the category `' . $cat_id . '`'); + Minz_Error::error(404); + } + } - Minz_Session::_param ('notification', $notif); + /** + * This action deletes a feed. + * + * This page must be reached by a POST request. + * If there are related queries, they are deleted too. + * + * Parameters are: + * - id (default: false) + * - r (default: false) + * r permits to redirect to a given page at the end of this action. + * + * @todo handle "r" redirection in Minz_Request::forward()? + */ + public function deleteAction() { + $redirect_url = Minz_Request::param('r', false, true); + if (!$redirect_url) { + $redirect_url = array('c' => 'subscription', 'a' => 'index'); + } - $redirect_url = Minz_Request::param('r', false, true); - if ($redirect_url) { - Minz_Request::forward($redirect_url); - } elseif ($type == 'category') { - Minz_Request::forward(array ('c' => 'configure', 'a' => 'categorize'), true); - } else { - Minz_Request::forward(array ('c' => 'configure', 'a' => 'feed'), true); - } + 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(); + + Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url); + } else { + Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url); } } } diff --git a/sources/app/Controllers/importExportController.php b/sources/app/Controllers/importExportController.php index f329766..589777b 100755 --- a/sources/app/Controllers/importExportController.php +++ b/sources/app/Controllers/importExportController.php @@ -1,12 +1,17 @@ view->loginOk) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } require_once(LIB_PATH . '/lib_opml.php'); @@ -16,13 +21,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->feedDAO = FreshRSS_Factory::createFeedDao(); } + /** + * This action displays the main page for import / export system. + */ public function indexAction() { - $this->view->categories = $this->catDAO->listCategories(); $this->view->feeds = $this->feedDAO->listFeeds(); - - Minz_View::prependTitle(_t('import_export') . ' · '); + 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); @@ -33,7 +48,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if ($status_file !== 0) { Minz_Log::error('File cannot be uploaded. Error code: ' . $status_file); - Minz_Request::bad(_t('file_cannot_be_uploaded'), + Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), array('c' => 'importExport', 'a' => 'index')); } @@ -55,7 +70,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if (!is_resource($zip)) { // zip_open cannot open file: something is wrong Minz_Log::error('Zip archive cannot be imported. Error code: ' . $zip); - Minz_Request::bad(_t('zip_error'), + Minz_Request::bad(_t('feedback.import_export.zip_error'), array('c' => 'importExport', 'a' => 'index')); } @@ -77,7 +92,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { zip_close($zip); } elseif ($type_file === 'zip') { // Zip extension is not loaded - Minz_Request::bad(_t('no_zip_extension'), + Minz_Request::bad(_t('feedback.import_export.no_zip_extension'), array('c' => 'importExport', 'a' => 'index')); } elseif ($type_file !== 'unknown') { $list_files[$type_file][] = file_get_contents($file['tmp_name']); @@ -92,24 +107,26 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $error = $this->importOpml($opml_file); } foreach ($list_files['json_starred'] as $article_file) { - $error = $this->importArticles($article_file, true); + $error = $this->importJson($article_file, true); } foreach ($list_files['json_feed'] as $article_file) { - $error = $this->importArticles($article_file); + $error = $this->importJson($article_file); } // And finally, we get import status and redirect to the home page Minz_Session::_param('actualize_feeds', true); - $content_notif = $error === true ? _t('feeds_imported_with_errors') : - _t('feeds_imported'); + $content_notif = $error === true ? _t('feedback.import_export.feeds_imported_with_errors') : + _t('feedback.import_export.feeds_imported'); Minz_Request::good($content_notif); } + /** + * This method tries to guess the file type based on its name. + * + * 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) { - // A *very* basic guess file type function. Only based on filename - // That's could be improved but should be enough, at least for a first - // implementation. - if (substr_compare($filename, '.zip', -4) === 0) { return 'zip'; } elseif (substr_compare($filename, '.opml', -5) === 0 || @@ -125,10 +142,16 @@ 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. + */ private function importOpml($opml_file) { $opml_array = array(); try { - $opml_array = libopml_parse_string($opml_file); + $opml_array = libopml_parse_string($opml_file, false); } catch (LibOPML_Exception $e) { Minz_Log::warning($e->getMessage()); return true; @@ -139,35 +162,78 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $this->addOpmlElements($opml_array['body']); } + /** + * This method imports an OPML file based on its body. + * + * @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. + */ private function addOpmlElements($opml_elements, $parent_cat = null) { $error = false; + + $nb_feeds = count($this->feedDAO->listFeeds()); + $nb_cats = count($this->catDAO->listCategories(false)); + $limits = FreshRSS_Context::$system_conf->limits; + foreach ($opml_elements as $elt) { - $res = false; + $is_error = false; if (isset($elt['xmlUrl'])) { - $res = $this->addFeedOpml($elt, $parent_cat); + // If xmlUrl exists, it means it is a feed + if ($nb_feeds >= $limits['max_feeds']) { + Minz_Log::warning(_t('feedback.sub.feed.over_max', + $limits['max_feeds'])); + $is_error = true; + continue; + } + + $is_error = $this->addFeedOpml($elt, $parent_cat); + if (!$is_error) { + $nb_feeds += 1; + } } else { - $res = $this->addCategoryOpml($elt, $parent_cat); + // No xmlUrl? It should be a category! + $limit_reached = ($nb_cats >= $limits['max_categories']); + if ($limit_reached) { + Minz_Log::warning(_t('feedback.sub.category.over_max', + $limits['max_categories'])); + } + + $is_error = $this->addCategoryOpml($elt, $parent_cat, $limit_reached); + if (!$is_error) { + $nb_cats += 1; + } } - if (!$error && $res) { + if (!$error && $is_error) { // oops: there is at least one error! - $error = $res; + $error = $is_error; } } return $error; } + /** + * This method imports an OPML feed element. + * + * @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. + */ private function addFeedOpml($feed_elt, $parent_cat) { + $default_cat = $this->catDAO->getDefault(); if (is_null($parent_cat)) { // This feed has no parent category so we get the default one - $parent_cat = $this->catDAO->getDefault()->name(); + $parent_cat = $default_cat->name(); } $cat = $this->catDAO->searchByName($parent_cat); - - if (!$cat) { - return true; + if (is_null($cat)) { + // If there is not $cat, it means parent category does not exist in + // database. + // If it happens, take the default category. + $cat = $default_cat; } // We get different useful information @@ -191,10 +257,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed->_website($website); $feed->_description($description); - // addFeedObject checks if feed is already in DB so nothing else to - // check here - $id = $this->feedDAO->addFeedObject($feed); - $error = ($id === false); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } else { + $error = true; + } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); $error = true; @@ -203,12 +275,24 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $error; } - private function addCategoryOpml($cat_elt, $parent_cat) { + /** + * This method imports an OPML category element. + * + * @param array $cat_elt an OPML element (must be a category element). + * @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. + */ + 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'])); - $id = $this->catDAO->addCategoryObject($cat); - $error = ($id === false); + $error = true; + if (!$cat_limit_reached) { + $id = $this->catDAO->addCategoryObject($cat); + $error = ($id === false); + } if (isset($cat_elt['@outlines'])) { // Our cat_elt contains more categories or more feeds, so we @@ -223,28 +307,55 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $error; } - private function importArticles($article_file, $starred = false) { + /** + * This method import a JSON-based file (Google Reader format). + * + * @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. + */ + 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; } - $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; - $google_compliant = ( - strpos($article_object['id'], 'com.google') !== false - ); + $google_compliant = strpos($article_object['id'], 'com.google') !== false; $error = false; $article_to_feed = array(); + $nb_feeds = count($this->feedDAO->listFeeds()); + $limits = FreshRSS_Context::$system_conf->limits; + // First, we check feeds of articles are in DB (and add them if needed). foreach ($article_object['items'] as $item) { - $feed = $this->addFeedArticles($item['origin'], $google_compliant); + $key = $google_compliant ? 'htmlUrl' : 'feedUrl'; + $feed = new FreshRSS_Feed($item['origin'][$key]); + $feed = $this->feedDAO->searchByUrl($feed->url()); + if (is_null($feed)) { - $error = true; - } else { + // Feed does not exist in DB,we should to try to add it. + if ($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)) { + // Still null? It means something went wrong. + $error = true; + } else { + // Nice! Increase the counter. + $nb_feeds += 1; + } + } + + if (!is_null($feed)) { $article_to_feed[$item['id']] = $feed->id(); } } @@ -254,6 +365,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->entryDAO->beginTransaction(); foreach ($article_object['items'] as $item) { if (!isset($article_to_feed[$item['id']])) { + // Related feed does not exist for this entry, do nothing. continue; } @@ -263,6 +375,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { 'summary' : 'content'; $tags = $item['categories']; 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; }); @@ -276,6 +389,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $entry->_id(min(time(), $entry->date(true)) . uSecString()); $entry->_tags($tags); + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + $values = $entry->toArray(); $id = $this->entryDAO->addEntry($values, $prepared_statement); @@ -288,7 +407,15 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $error; } - private function addFeedArticles($origin, $google_compliant) { + /** + * This method import a JSON-based feed (Google Reader format). + * + * @param array $origin represents a feed. + * @param boolean $google_compliant takes care of some specific values if true. + * @return FreshRSS_Feed if feed is in database at the end of the process, + * else null. + */ + private function addFeedJson($origin, $google_compliant) { $default_cat = $this->catDAO->getDefault(); $return = null; @@ -298,19 +425,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $website = $origin['htmlUrl']; try { - // Create a Feed object and add it in DB + // Create a Feed object and add it in database. $feed = new FreshRSS_Feed($url); $feed->_category($default_cat->id()); $feed->_name($name); $feed->_website($website); - // addFeedObject checks if feed is already in DB so nothing else to - // check here - $id = $this->feedDAO->addFeedObject($feed); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here. + $id = $this->feedDAO->addFeedObject($feed); - if ($id !== false) { - $feed->_id($id); - $return = $feed; + if ($id !== false) { + $feed->_id($id); + $return = $feed; + } } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); @@ -319,6 +450,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { 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); @@ -336,7 +477,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } if ($export_starred) { - $export_files['starred.json'] = $this->generateArticles('starred'); + $export_files['starred.json'] = $this->generateEntries('starred'); } foreach ($export_feeds as $feed_id) { @@ -344,9 +485,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if ($feed) { $filename = 'feed_' . $feed->category() . '_' . $feed->id() . '.json'; - $export_files[$filename] = $this->generateArticles( - 'feed', $feed - ); + $export_files[$filename] = $this->generateEntries('feed', $feed); } } @@ -357,7 +496,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->exportZip($export_files); } catch (Exception $e) { # Oops, there is no Zip extension! - Minz_Request::bad(_t('export_no_zip_extension'), + Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'), array('c' => 'importExport', 'a' => 'index')); } } elseif ($nb_files === 1) { @@ -366,10 +505,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $type = $this->guessFileType($filename); $this->exportFile('freshrss_' . $filename, $export_files[$filename], $type); } else { + // Nothing to do... Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); } } + /** + * This method returns the OPML file based on user subscriptions. + * + * @return string the OPML file content. + */ private function generateOpml() { $list = array(); foreach ($this->catDAO->listCategories() as $key => $cat) { @@ -381,23 +526,29 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $this->view->helperToString('export/opml'); } - private function generateArticles($type, $feed = NULL) { + /** + * This method returns a JSON file content. + * + * @param string $type must be "starred" or "feed" + * @param FreshRSS_Feed $feed feed of which we want to get entries. + * @return string the JSON file content. + */ + private function generateEntries($type, $feed = NULL) { $this->view->categories = $this->catDAO->listCategories(); if ($type == 'starred') { - $this->view->list_title = _t('starred_list'); + $this->view->list_title = _t('sub.import_export.starred_list'); $this->view->type = 'starred'; $unread_fav = $this->entryDAO->countUnreadReadFavorites(); $this->view->entries = $this->entryDAO->listWhere( - 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', - $unread_fav['all'] + 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all'] ); } elseif ($type == 'feed' && !is_null($feed)) { - $this->view->list_title = _t('feed_list', $feed->name()); + $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', - $this->view->conf->posts_per_page + FreshRSS_Context::$user_conf->posts_per_page ); $this->view->feed = $feed; } @@ -405,6 +556,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $this->view->helperToString('export/articles'); } + /** + * This method zips a list of files and returns it by HTTP. + * + * @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) { if (!extension_loaded('zip')) { throw new Exception(); @@ -428,6 +585,14 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { unlink($zip_file); } + /** + * This method returns a single file (OPML or JSON) by HTTP. + * + * @param string $filename + * @param string $content + * @param string $type the file type (opml, json_feed or json_starred). + * If equals to unknown, nothing happens. + */ private function exportFile($filename, $content, $type) { if ($type === 'unknown') { return; diff --git a/sources/app/Controllers/indexController.php b/sources/app/Controllers/indexController.php index d3b299f..ddcf0b4 100755 --- a/sources/app/Controllers/indexController.php +++ b/sources/app/Controllers/indexController.php @@ -1,499 +1,236 @@ view->conf->token; - - // check if user is logged in - if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) { - $token_param = Minz_Request::param ('token', ''); - $token_is_ok = ($token != '' && $token === $token_param); - if ($output === 'rss' && !$token_is_ok) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); - return; - } elseif ($output !== 'rss') { - // "hard" redirection is not required, just ask dispatcher to - // forward to the login form without 302 redirection - Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin')); - return; - } - } - - $params = Minz_Request::params (); - if (isset ($params['search'])) { - $params['search'] = urlencode ($params['search']); - } - - $this->view->url = array ( + /** + * This action only redirect on the default view mode (normal or global) + */ + public function indexAction() { + $prefered_output = FreshRSS_Context::$user_conf->view_mode; + Minz_Request::forward(array( 'c' => 'index', - 'a' => 'index', - 'params' => $params - ); + 'a' => $prefered_output + )); + } - if ($output === 'rss') { - // no layout for RSS output - $this->view->_useLayout (false); - header('Content-Type: application/rss+xml; charset=utf-8'); - } elseif ($output === 'global') { - Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js'))); - } - - $catDAO = new FreshRSS_CategoryDAO(); - $entryDAO = FreshRSS_Factory::createEntryDao(); - - $this->view->cat_aside = $catDAO->listCategories (); - $this->view->nb_favorites = $entryDAO->countUnreadReadFavorites (); - $this->view->nb_not_read = FreshRSS_CategoryDAO::CountUnreads($this->view->cat_aside, 1); - $this->view->currentName = ''; - - $this->view->get_c = ''; - $this->view->get_f = ''; - - $get = Minz_Request::param ('get', 'a'); - $getType = $get[0]; - $getId = substr ($get, 2); - if (!$this->checkAndProcessType ($getType, $getId)) { - Minz_Log::record ('Not found [' . $getType . '][' . $getId . ']', Minz_Log::DEBUG); - Minz_Error::error ( - 404, - array ('error' => array (Minz_Translate::t ('page_not_found'))) - ); + /** + * This action displays the normal view of FreshRSS. + */ + public function normalAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) { + Minz_Request::forward(array('c' => 'auth', 'a' => 'login')); return; } - // mise à jour des titres - $this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title(); - Minz_View::prependTitle( - ($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') . - $this->view->currentName . - ' · ' - ); - - // On récupère les différents éléments de filtrage - $this->view->state = Minz_Request::param('state', $this->view->conf->default_view); - $state_param = Minz_Request::param ('state', null); - $filter = Minz_Request::param ('search', ''); - $this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order); - $nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page); - $first = Minz_Request::param ('next', ''); - - $ajax_request = Minz_Request::param('ajax', false); - if ($output === 'reader') { - $nb = max(1, round($nb / 2)); + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); } - if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ) { //Any unread article in this category at all? - switch ($getType) { - case 'a': - $hasUnread = $this->view->nb_not_read > 0; - break; - case 's': - // This is deprecated. The favorite button does not exist anymore - $hasUnread = $this->view->nb_favorites['unread'] > 0; - break; - case 'c': - $hasUnread = (!isset($this->view->cat_aside[$getId])) || ($this->view->cat_aside[$getId]->nbNotRead() > 0); - break; - case 'f': - $myFeed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId); - $hasUnread = ($myFeed === null) || ($myFeed->nbNotRead() > 0); - break; - default: - $hasUnread = true; - break; - } - if (!$hasUnread && ($state_param === null)) { - $this->view->state = FreshRSS_Entry::STATE_ALL; - } - } - - $today = @strtotime('today'); - $this->view->today = $today; - - // on calcule la date des articles les plus anciens qu'on affiche - $nb_month_old = $this->view->conf->old_entries; - $date_min = $today - (3600 * 24 * 30 * $nb_month_old); //Do not use a fast changing value such as time() to allow SQL caching - $keepHistoryDefault = $this->view->conf->keep_history_default; - try { - $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault); + $entries = $this->listEntriesByContext(); - // Si on a récupéré aucun article "non lus" - // on essaye de récupérer tous les articles - if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) { - Minz_Log::record('Conflicting information about nbNotRead!', Minz_Log::DEBUG); - $feedDAO = FreshRSS_Factory::createFeedDao(); - try { - $feedDAO->updateCachedValues(); - } catch (Exception $ex) { - Minz_Log::record('Failed to automatically correct nbNotRead! ' + $ex->getMessage(), Minz_Log::NOTICE); - } - $this->view->state = FreshRSS_Entry::STATE_ALL; - $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault); + $nb_entries = count($entries); + if ($nb_entries > FreshRSS_Context::$number) { + // We have more elements for pagination + $last_entry = array_pop($entries); + FreshRSS_Context::$next_id = $last_entry->id(); } - Minz_Request::_param('state', $this->view->state); - if (count($entries) <= $nb) { - $this->view->nextId = ''; - } else { //We have more elements for pagination - $lastEntry = array_pop($entries); - $this->view->nextId = $lastEntry->id(); + $first_entry = $nb_entries > 0 ? $entries[0] : null; + FreshRSS_Context::$id_max = $first_entry === null ? + (time() - 1) . '000000' : + $first_entry->id(); + if (FreshRSS_Context::$order === 'ASC') { + // In this case we do not know but we guess id_max + $id_max = (time() - 1) . '000000'; + if (strcmp($id_max, FreshRSS_Context::$id_max) > 0) { + FreshRSS_Context::$id_max = $id_max; + } } $this->view->entries = $entries; } catch (FreshRSS_EntriesGetter_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE); - Minz_Error::error ( - 404, - array ('error' => array (Minz_Translate::t ('page_not_found'))) - ); + Minz_Log::notice($e->getMessage()); + Minz_Error::error(404); } + + $this->view->categories = FreshRSS_Context::$categories; + + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $title = FreshRSS_Context::$name; + if (FreshRSS_Context::$get_unread > 0) { + $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; + } + Minz_View::prependTitle($title . ' · '); } - /* - * Vérifie que la catégorie / flux sélectionné existe - * + Initialise correctement les variables de vue get_c et get_f - * + Met à jour la variable $this->nb_not_read_cat + /** + * This action displays the reader view of FreshRSS. + * + * @todo: change this view into specific CSS rules? */ - private function checkAndProcessType ($getType, $getId) { - switch ($getType) { - case 'a': - $this->view->currentName = Minz_Translate::t ('your_rss_feeds'); - $this->nb_not_read_cat = $this->view->nb_not_read; - $this->view->get_c = $getType; - return true; - case 's': - $this->view->currentName = Minz_Translate::t ('your_favorites'); - $this->nb_not_read_cat = $this->view->nb_favorites['unread']; - $this->view->get_c = $getType; - return true; - case 'c': - $cat = isset($this->view->cat_aside[$getId]) ? $this->view->cat_aside[$getId] : null; - if ($cat === null) { - $catDAO = new FreshRSS_CategoryDAO(); - $cat = $catDAO->searchById($getId); - } - if ($cat) { - $this->view->currentName = $cat->name (); - $this->nb_not_read_cat = $cat->nbNotRead (); - $this->view->get_c = $getId; - return true; - } else { - return false; - } - case 'f': - $feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId); - if (empty($feed)) { - $feedDAO = FreshRSS_Factory::createFeedDao(); - $feed = $feedDAO->searchById($getId); - } - if ($feed) { - $this->view->currentName = $feed->name (); - $this->nb_not_read_cat = $feed->nbNotRead (); - $this->view->get_f = $getId; - $this->view->get_c = $feed->category (); - return true; - } else { - return false; - } - default: - return false; - } - } - - public function aboutAction () { - Minz_View::prependTitle (Minz_Translate::t ('about') . ' · '); + public function readerAction() { + $this->normalAction(); } - public function logsAction () { - if (!$this->view->loginOk) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); + /** + * This action displays the global view of FreshRSS. + */ + public function globalAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) { + Minz_Request::forward(array('c' => 'auth', 'a' => 'login')); + return; } - Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · '); + Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js'))); - if (Minz_Request::isPost ()) { + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); + } + + $this->view->categories = FreshRSS_Context::$categories; + + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $title = _t('index.feed.title_global'); + if (FreshRSS_Context::$get_unread > 0) { + $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; + } + Minz_View::prependTitle($title . ' · '); + } + + /** + * This action displays the RSS feed of FreshRSS. + */ + public function rssAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + $token = FreshRSS_Context::$user_conf->token; + $token_param = Minz_Request::param('token', ''); + $token_is_ok = ($token != '' && $token === $token_param); + + // Check if user has access. + if (!FreshRSS_Auth::hasAccess() && + !$allow_anonymous && + !$token_is_ok) { + Minz_Error::error(403); + } + + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); + } + + try { + $this->view->entries = $this->listEntriesByContext(); + } catch (FreshRSS_EntriesGetter_Exception $e) { + Minz_Log::notice($e->getMessage()); + Minz_Error::error(404); + } + + // No layout for RSS output. + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $this->view->_useLayout(false); + header('Content-Type: application/rss+xml; charset=utf-8'); + } + + /** + * This action updates the Context object by using request parameters. + * + * Parameters are: + * - state (default: conf->default_view) + * - search (default: empty string) + * - order (default: conf->sort_order) + * - nb (default: conf->posts_per_page) + * - next (default: empty string) + */ + private function updateContext() { + // Update number of read / unread variables. + $entryDAO = FreshRSS_Factory::createEntryDao(); + FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites(); + FreshRSS_Context::$total_unread = FreshRSS_CategoryDAO::CountUnreads( + FreshRSS_Context::$categories, 1 + ); + + FreshRSS_Context::_get(Minz_Request::param('get', 'a')); + FreshRSS_Context::$state = Minz_Request::param( + 'state', FreshRSS_Context::$user_conf->default_state + ); + $state_forced_by_user = Minz_Request::param('state', false) !== false; + if (FreshRSS_Context::$user_conf->default_view === 'adaptive' && + FreshRSS_Context::$get_unread <= 0 && + !FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ) && + !$state_forced_by_user) { + FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ; + } + + FreshRSS_Context::$search = Minz_Request::param('search', ''); + 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::$first_id = Minz_Request::param('next', ''); + } + + /** + * This method returns a list of entries based on the Context object. + */ + private function listEntriesByContext() { + $entryDAO = FreshRSS_Factory::createEntryDao(); + + $get = FreshRSS_Context::currentGet(true); + if (count($get) > 1) { + $type = $get[0]; + $id = $get[1]; + } else { + $type = $get; + $id = ''; + } + + return $entryDAO->listWhere( + $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order, + FreshRSS_Context::$number + 1, FreshRSS_Context::$first_id, + FreshRSS_Context::$search + ); + } + + /** + * This action displays the about page of FreshRSS. + */ + public function aboutAction() { + Minz_View::prependTitle(_t('index.about.title') . ' · '); + } + + /** + * This action displays logs of FreshRSS for the current user. + */ + public function logsAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + Minz_View::prependTitle(_t('index.log.title') . ' · '); + + if (Minz_Request::isPost()) { FreshRSS_LogDAO::truncate(); } $logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines //gestion pagination - $page = Minz_Request::param ('page', 1); - $this->view->logsPaginator = new Minz_Paginator ($logs); - $this->view->logsPaginator->_nbItemsPerPage (50); - $this->view->logsPaginator->_currentPage ($page); - } - - public function loginAction () { - $this->view->_useLayout (false); - - $url = 'https://verifier.login.persona.org/verify'; - $assert = Minz_Request::param ('assertion'); - $params = 'assertion=' . $assert . '&audience=' . - urlencode (Minz_Url::display (null, 'php', true)); - $ch = curl_init (); - $options = array ( - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => TRUE, - CURLOPT_POST => 2, - CURLOPT_POSTFIELDS => $params - ); - curl_setopt_array ($ch, $options); - $result = curl_exec ($ch); - curl_close ($ch); - - $res = json_decode ($result, true); - - $loginOk = false; - $reason = ''; - if ($res['status'] === 'okay') { - $email = filter_var($res['email'], FILTER_VALIDATE_EMAIL); - if ($email != '') { - $personaFile = DATA_PATH . '/persona/' . $email . '.txt'; - if (($currentUser = @file_get_contents($personaFile)) !== false) { - $currentUser = trim($currentUser); - if (ctype_alnum($currentUser)) { - try { - $this->conf = new FreshRSS_Configuration($currentUser); - $loginOk = strcasecmp($email, $this->conf->mail_login) === 0; - } catch (Minz_Exception $e) { - $reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage(); //Permission denied or conf file does not exist - } - } else { - $reason = 'Invalid username format [' . $currentUser . ']!'; - } - } - } else { - $reason = 'Invalid email format [' . $res['email'] . ']!'; - } - } - if ($loginOk) { - Minz_Session::_param('currentUser', $currentUser); - Minz_Session::_param ('mail', $email); - $this->view->loginOk = true; - invalidateHttpCache(); - } else { - $res = array (); - $res['status'] = 'failure'; - $res['reason'] = $reason == '' ? Minz_Translate::t ('invalid_login') : $reason; - Minz_Log::record ('Persona: ' . $res['reason'], Minz_Log::WARNING); - } - - header('Content-Type: application/json; charset=UTF-8'); - $this->view->res = json_encode ($res); - } - - public function logoutAction () { - $this->view->_useLayout(false); - invalidateHttpCache(); - Minz_Session::_param('currentUser'); - Minz_Session::_param('mail'); - Minz_Session::_param('passwordHash'); - } - - private static function makeLongTermCookie($username, $passwordHash) { - do { - $token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true)); - $tokenFile = DATA_PATH . '/tokens/' . $token . '.txt'; - } while (file_exists($tokenFile)); - if (@file_put_contents($tokenFile, $username . "\t" . $passwordHash) === false) { - return false; - } - $expire = time() + 2629744; //1 month //TODO: Use a configuration instead - Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire); - Minz_Session::_param('token', $token); - return $token; - } - - private static function deleteLongTermCookie() { - Minz_Session::deleteLongTermCookie('FreshRSS_login'); - $token = Minz_Session::param('token', null); - if (ctype_alnum($token)) { - @unlink(DATA_PATH . '/tokens/' . $token . '.txt'); - } - Minz_Session::_param('token'); - if (rand(0, 10) === 1) { - self::purgeTokens(); - } - } - - private static function purgeTokens() { - $oldest = time() - 2629744; //1 month //TODO: Use a configuration instead - foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $fileInfo) { - $extension = pathinfo($fileInfo->getFilename(), PATHINFO_EXTENSION); - if ($extension === 'txt' && $fileInfo->getMTime() < $oldest) { - @unlink($fileInfo->getPathname()); - } - } - } - - public function formLoginAction () { - if ($this->view->loginOk) { - Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); - } - - if (Minz_Request::isPost()) { - $ok = false; - $nonce = Minz_Session::param('nonce'); - $username = Minz_Request::param('username', ''); - $c = Minz_Request::param('challenge', ''); - if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) { - if (!function_exists('password_verify')) { - include_once(LIB_PATH . '/password_compat.php'); - } - try { - $conf = new FreshRSS_Configuration($username); - $s = $conf->passwordHash; - $ok = password_verify($nonce . $s, $c); - if ($ok) { - Minz_Session::_param('currentUser', $username); - Minz_Session::_param('passwordHash', $s); - if (Minz_Request::param('keep_logged_in', false)) { - self::makeLongTermCookie($username, $s); - } else { - self::deleteLongTermCookie(); - } - } else { - Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING); - } - } catch (Minz_Exception $me) { - Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING); - } - } else { - Minz_Log::record('Invalid credential parameters: user=' . $username . ' challenge=' . $c . ' nonce=' . $nonce, Minz_Log::DEBUG); - } - if (!$ok) { - $notif = array( - 'type' => 'bad', - 'content' => Minz_Translate::t('invalid_login') - ); - Minz_Session::_param('notification', $notif); - } - $this->view->_useLayout(false); - Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); - } elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) { - Minz_Session::_param('currentUser'); - Minz_Session::_param('mail'); - Minz_Session::_param('passwordHash'); - $username = ctype_alnum($_GET['u']) ? $_GET['u'] : ''; - $passwordPlain = $_GET['p']; - Minz_Request::_param('p'); //Discard plain-text password ASAP - $_GET['p'] = ''; - if (!function_exists('password_verify')) { - include_once(LIB_PATH . '/password_compat.php'); - } - try { - $conf = new FreshRSS_Configuration($username); - $s = $conf->passwordHash; - $ok = password_verify($passwordPlain, $s); - unset($passwordPlain); - if ($ok) { - Minz_Session::_param('currentUser', $username); - Minz_Session::_param('passwordHash', $s); - } else { - Minz_Log::record('Unsafe password mismatch for user ' . $username, Minz_Log::WARNING); - } - } catch (Minz_Exception $me) { - Minz_Log::record('Unsafe login failure: ' . $me->getMessage(), Minz_Log::WARNING); - } - Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); - } elseif (!Minz_Configuration::canLogIn()) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); - } - invalidateHttpCache(); - } - - public function formLogoutAction () { - $this->view->_useLayout(false); - invalidateHttpCache(); - Minz_Session::_param('currentUser'); - Minz_Session::_param('mail'); - Minz_Session::_param('passwordHash'); - self::deleteLongTermCookie(); - Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); - } - - public function resetAuthAction() { - Minz_View::prependTitle(_t('auth_reset') . ' · '); - Minz_View::appendScript(Minz_Url::display( - '/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js') - )); - - $this->view->no_form = false; - // Enable changement of auth only if Persona! - if (Minz_Configuration::authType() != 'persona') { - $this->view->message = array( - 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('auth_not_persona') - ); - $this->view->no_form = true; - return; - } - - $conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser()); - // Admin user must have set its master password. - if (!$conf->passwordHash) { - $this->view->message = array( - 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('auth_no_password_set') - ); - $this->view->no_form = true; - return; - } - - invalidateHttpCache(); - - if (Minz_Request::isPost()) { - $nonce = Minz_Session::param('nonce'); - $username = Minz_Request::param('username', ''); - $c = Minz_Request::param('challenge', ''); - if (!(ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce))) { - Minz_Log::debug('Invalid credential parameters:' . - ' user=' . $username . - ' challenge=' . $c . - ' nonce=' . $nonce); - Minz_Request::bad(_t('invalid_login'), - array('c' => 'index', 'a' => 'resetAuth')); - } - - if (!function_exists('password_verify')) { - include_once(LIB_PATH . '/password_compat.php'); - } - - $s = $conf->passwordHash; - $ok = password_verify($nonce . $s, $c); - if ($ok) { - Minz_Configuration::_authType('form'); - $ok = Minz_Configuration::writeFile(); - - if ($ok) { - Minz_Request::good(_t('auth_form_set')); - } else { - Minz_Request::bad(_t('auth_form_not_set'), - array('c' => 'index', 'a' => 'resetAuth')); - } - } else { - Minz_Log::debug('Password mismatch for user ' . $username . - ', nonce=' . $nonce . ', c=' . $c); - - Minz_Request::bad(_t('invalid_login'), - array('c' => 'index', 'a' => 'resetAuth')); - } - } + $page = Minz_Request::param('page', 1); + $this->view->logsPaginator = new Minz_Paginator($logs); + $this->view->logsPaginator->_nbItemsPerPage(50); + $this->view->logsPaginator->_currentPage($page); } } diff --git a/sources/app/Controllers/javascriptController.php b/sources/app/Controllers/javascriptController.php index 6714835..421cf6f 100755 --- a/sources/app/Controllers/javascriptController.php +++ b/sources/app/Controllers/javascriptController.php @@ -1,14 +1,14 @@ view->_useLayout (false); + public function firstAction() { + $this->view->_useLayout(false); } - public function actualizeAction () { + public function actualizeAction() { header('Content-Type: text/javascript; charset=UTF-8'); $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->view->feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default); + $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } public function nbUnreadsPerFeedAction() { @@ -28,17 +28,20 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { $user = isset($_GET['user']) ? $_GET['user'] : ''; if (ctype_alnum($user)) { try { - $conf = new FreshRSS_Configuration($user); + $salt = FreshRSS_Context::$system_conf->salt; + $conf = get_user_configuration($user); $s = $conf->passwordHash; if (strlen($s) >= 60) { $this->view->salt1 = substr($s, 0, 29); //CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". - $this->view->nonce = sha1(Minz_Configuration::salt() . uniqid(mt_rand(), true)); + $this->view->nonce = sha1($salt . uniqid(mt_rand(), true)); Minz_Session::_param('nonce', $this->view->nonce); return; //Success } } catch (Minz_Exception $me) { - Minz_Log::record('Nonce failure: ' . $me->getMessage(), Minz_Log::WARNING); + Minz_Log::warning('Nonce failure: ' . $me->getMessage()); } + } else { + Minz_Log::notice('Nonce failure due to invalid username!'); } $this->view->nonce = ''; //Failure $this->view->salt1 = ''; diff --git a/sources/app/Controllers/statsController.php b/sources/app/Controllers/statsController.php index 3069be3..4a597ae 100755 --- a/sources/app/Controllers/statsController.php +++ b/sources/app/Controllers/statsController.php @@ -5,6 +5,19 @@ */ class FreshRSS_stats_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + Minz_View::prependTitle(_t('admin.stats.title') . ' · '); + } + /** * This action handles the statistic main page. * @@ -99,11 +112,12 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $categoryDAO = new FreshRSS_CategoryDAO(); $feedDAO = FreshRSS_Factory::createFeedDao(); Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js'))); - $id = Minz_Request::param ('id', null); + $id = Minz_Request::param('id', null); $this->view->categories = $categoryDAO->listCategories(); $this->view->feed = $feedDAO->searchById($id); $this->view->days = $statsDAO->getDays(); $this->view->months = $statsDAO->getMonths(); + $this->view->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id); $this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id); $this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id); $this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id); @@ -111,20 +125,4 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id); $this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id); } - - /** - * This action is called before every other action in that class. It is - * the common boiler plate for every action. It is triggered by the - * underlying framework. - */ - public function firstAction() { - if (!$this->view->loginOk) { - Minz_Error::error( - 403, array('error' => array(Minz_Translate::t('access_denied'))) - ); - } - - Minz_View::prependTitle(Minz_Translate::t('stats') . ' · '); - } - } diff --git a/sources/app/Controllers/subscriptionController.php b/sources/app/Controllers/subscriptionController.php new file mode 100755 index 0000000..333565f --- /dev/null +++ b/sources/app/Controllers/subscriptionController.php @@ -0,0 +1,116 @@ +checkDefault(); + $this->view->categories = $catDAO->listCategories(false); + $this->view->default_category = $catDAO->getDefault(); + } + + /** + * This action handles the main subscription page + * + * It displays categories and associated feeds. + */ + public function indexAction() { + Minz_View::appendScript(Minz_Url::display('/scripts/category.js?' . + @filemtime(PUBLIC_PATH . '/scripts/category.js'))); + Minz_View::prependTitle(_t('sub.title') . ' · '); + + $id = Minz_Request::param('id'); + if ($id !== false) { + $feedDAO = FreshRSS_Factory::createFeedDao(); + $this->view->feed = $feedDAO->searchById($id); + } + } + + /** + * This action handles the feed configuration page. + * + * It displays the feed configuration page. + * If this action is reached through a POST request, it stores all new + * configuraiton values then sends a notification to the user. + * + * The options available on the page are: + * - name + * - description + * - website URL + * - feed URL + * - category id (default: default category id) + * - CSS path to article on website + * - display in main stream (default: 0) + * - HTTP authentication + * - number of article to retain (default: -2) + * - refresh frequency (default: -2) + * Default values are empty strings unless specified. + */ + public function feedAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } + + $feedDAO = FreshRSS_Factory::createFeedDao(); + $this->view->feeds = $feedDAO->listFeeds(); + + $id = Minz_Request::param('id'); + if ($id === false || !isset($this->view->feeds[$id])) { + Minz_Error::error(404); + return; + } + + $this->view->feed = $this->view->feeds[$id]; + + Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · '); + + if (Minz_Request::isPost()) { + $user = Minz_Request::param('http_user', ''); + $pass = Minz_Request::param('http_pass', ''); + + $httpAuth = ''; + if ($user != '' || $pass != '') { + $httpAuth = $user . ':' . $pass; + } + + $cat = intval(Minz_Request::param('category', 0)); + + $values = array( + 'name' => Minz_Request::param('name', ''), + 'description' => sanitizeHTML(Minz_Request::param('description', '', true)), + 'website' => Minz_Request::param('website', ''), + 'url' => Minz_Request::param('url', ''), + 'category' => $cat, + 'pathEntries' => Minz_Request::param('path_entries', ''), + 'priority' => intval(Minz_Request::param('priority', 0)), + 'httpAuth' => $httpAuth, + 'keep_history' => intval(Minz_Request::param('keep_history', -2)), + 'ttl' => intval(Minz_Request::param('ttl', -2)), + ); + + invalidateHttpCache(); + + $url_redirect = array('c' => 'subscription', 'params' => array('id' => $id)); + if ($feedDAO->updateFeed($id, $values) !== false) { + $this->view->feed->_category($cat); + $this->view->feed->faviconPrepare(); + + Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect); + } + } + } +} diff --git a/sources/app/Controllers/updateController.php b/sources/app/Controllers/updateController.php index da5bddc..4797a34 100755 --- a/sources/app/Controllers/updateController.php +++ b/sources/app/Controllers/updateController.php @@ -1,42 +1,42 @@ view->loginOk && Minz_Configuration::isAdmin($current_user)) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); } invalidateHttpCache(); - Minz_View::prependTitle(_t('update_system') . ' · '); $this->view->update_to_apply = false; $this->view->last_update_time = 'unknown'; - $this->view->check_last_hour = false; - $timestamp = (int)@file_get_contents(DATA_PATH . '/last_update.txt'); - if (is_numeric($timestamp) && $timestamp > 0) { + $timestamp = @filemtime(join_path(DATA_PATH, 'last_update.txt')); + if ($timestamp !== false) { $this->view->last_update_time = timestamptodate($timestamp); - $this->view->check_last_hour = (time() - 3600) <= $timestamp; } } public function indexAction() { + Minz_View::prependTitle(_t('admin.update.title') . ' · '); + if (file_exists(UPDATE_FILENAME) && !is_writable(FRESHRSS_PATH)) { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('file_is_nok', FRESHRSS_PATH) + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.file_is_nok', FRESHRSS_PATH) ); } elseif (file_exists(UPDATE_FILENAME)) { // There is an update file to apply! + $version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt')); + if (empty($version)) { + $version = 'unknown'; + } $this->view->update_to_apply = true; $this->view->message = array( 'status' => 'good', - 'title' => _t('ok'), - 'body' => _t('update_can_apply') + 'title' => _t('gen.short.ok'), + 'body' => _t('feedback.update.can_apply', $version) ); } } @@ -44,11 +44,11 @@ class FreshRSS_update_Controller extends Minz_ActionController { public function checkAction() { $this->view->change_view('update', 'index'); - if (file_exists(UPDATE_FILENAME) || $this->view->check_last_hour) { + if (file_exists(UPDATE_FILENAME)) { // There is already an update file to apply: we don't need to check // the webserver! // Or if already check during the last hour, do nothing. - Minz_Request::forward(array('c' => 'update')); + Minz_Request::forward(array('c' => 'update'), true); return; } @@ -69,8 +69,8 @@ class FreshRSS_update_Controller extends Minz_ActionController { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('update_server_not_found', FRESHRSS_UPDATE_WEBSITE) + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.server_not_found', FRESHRSS_UPDATE_WEBSITE) ); return; } @@ -80,23 +80,27 @@ class FreshRSS_update_Controller extends Minz_ActionController { if (strpos($status, 'UPDATE') !== 0) { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('no_update') + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.none') ); - @file_put_contents(DATA_PATH . '/last_update.txt', time()); + @touch(join_path(DATA_PATH, 'last_update.txt')); return; } $script = $res_array[1]; if (file_put_contents(UPDATE_FILENAME, $script) !== false) { - Minz_Request::forward(array('c' => 'update')); + $version = explode(' ', $status, 2); + $version = $version[1]; + @file_put_contents(join_path(DATA_PATH, 'last_update.txt'), $version); + + Minz_Request::forward(array('c' => 'update'), true); } else { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('update_problem', 'Cannot save the update script') + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.error', 'Cannot save the update script') ); } } @@ -108,6 +112,21 @@ class FreshRSS_update_Controller extends Minz_ActionController { require(UPDATE_FILENAME); + if (Minz_Request::param('post_conf', false)) { + $res = do_post_update(); + + Minz_ExtensionManager::callHook('post_update'); + + if ($res === true) { + @unlink(UPDATE_FILENAME); + @file_put_contents(join_path(DATA_PATH, 'last_update.txt'), ''); + Minz_Request::good(_t('feedback.update.finished')); + } else { + Minz_Request::bad(_t('feedback.update.error', $res), + array('c' => 'update', 'a' => 'index')); + } + } + if (Minz_Request::isPost()) { save_info_update(); } @@ -116,14 +135,26 @@ class FreshRSS_update_Controller extends Minz_ActionController { $res = apply_update(); if ($res === true) { - @unlink(UPDATE_FILENAME); - @file_put_contents(DATA_PATH . '/last_update.txt', time()); - - Minz_Request::good(_t('update_finished')); + Minz_Request::forward(array( + 'c' => 'update', + 'a' => 'apply', + 'params' => array('post_conf' => true) + ), true); } else { - Minz_Request::bad(_t('update_problem', $res), + Minz_Request::bad(_t('feedback.update.error', $res), array('c' => 'update', 'a' => 'index')); } } } + + /** + * This action displays information about installation. + */ + public function checkInstallAction() { + Minz_View::prependTitle(_t('admin.check_install.title') . ' · '); + + $this->view->status_php = check_install_php(); + $this->view->status_files = check_install_files(); + $this->view->status_database = check_install_database(); + } } diff --git a/sources/app/Controllers/usersController.php b/sources/app/Controllers/userController.php similarity index 51% rename from sources/app/Controllers/usersController.php rename to sources/app/Controllers/userController.php index a9e6c32..ed01b83 100755 --- a/sources/app/Controllers/usersController.php +++ b/sources/app/Controllers/userController.php @@ -1,19 +1,30 @@ view->loginOk) { - Minz_Error::error( - 403, - array('error' => array(Minz_Translate::t('access_denied'))) - ); + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } } - public function authAction() { + /** + * This action displays the user profile page. + */ + public function profileAction() { + Minz_View::prependTitle(_t('conf.profile.title') . ' · '); + if (Minz_Request::isPost()) { $ok = true; @@ -28,9 +39,9 @@ class FreshRSS_users_Controller extends Minz_ActionController { $passwordPlain = ''; $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js $ok &= ($passwordHash != ''); - $this->view->conf->_passwordHash($passwordHash); + FreshRSS_Context::$user_conf->passwordHash = $passwordHash; } - Minz_Session::_param('passwordHash', $this->view->conf->passwordHash); + Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash); $passwordPlain = Minz_Request::param('apiPasswordPlain', '', true); if ($passwordPlain != '') { @@ -41,16 +52,17 @@ class FreshRSS_users_Controller extends Minz_ActionController { $passwordPlain = ''; $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js $ok &= ($passwordHash != ''); - $this->view->conf->_apiPasswordHash($passwordHash); + FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash; } - if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $this->view->conf->_mail_login(Minz_Request::param('mail_login', '', true)); + // TODO: why do we need of hasAccess here? + if (FreshRSS_Auth::hasAccess('admin')) { + FreshRSS_Context::$user_conf->mail_login = Minz_Request::param('mail_login', '', true); } - $email = $this->view->conf->mail_login; + $email = FreshRSS_Context::$user_conf->mail_login; Minz_Session::_param('mail', $email); - $ok &= $this->view->conf->save(); + $ok &= FreshRSS_Context::$user_conf->save(); if ($email != '') { $personaFile = DATA_PATH . '/persona/' . $email . '.txt'; @@ -58,68 +70,63 @@ class FreshRSS_users_Controller extends Minz_ActionController { $ok &= (file_put_contents($personaFile, Minz_Session::param('currentUser', '_')) !== false); } - if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $current_token = $this->view->conf->token; - $token = Minz_Request::param('token', $current_token); - $this->view->conf->_token($token); - $ok &= $this->view->conf->save(); - - $anon = Minz_Request::param('anon_access', false); - $anon = ((bool)$anon) && ($anon !== 'no'); - $anon_refresh = Minz_Request::param('anon_refresh', false); - $anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no'); - $auth_type = Minz_Request::param('auth_type', 'none'); - $unsafe_autologin = Minz_Request::param('unsafe_autologin', false); - $api_enabled = Minz_Request::param('api_enabled', false); - if ($anon != Minz_Configuration::allowAnonymous() || - $auth_type != Minz_Configuration::authType() || - $anon_refresh != Minz_Configuration::allowAnonymousRefresh() || - $unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() || - $api_enabled != Minz_Configuration::apiEnabled()) { - - Minz_Configuration::_authType($auth_type); - Minz_Configuration::_allowAnonymous($anon); - Minz_Configuration::_allowAnonymousRefresh($anon_refresh); - Minz_Configuration::_enableAutologin($unsafe_autologin); - Minz_Configuration::_enableApi($api_enabled); - $ok &= Minz_Configuration::writeFile(); - } + if ($ok) { + Minz_Request::good(_t('feedback.profile.updated'), + array('c' => 'user', 'a' => 'profile')); + } else { + Minz_Request::bad(_t('feedback.profile.error'), + array('c' => 'user', 'a' => 'profile')); } - - invalidateHttpCache(); - - $notif = array( - 'type' => $ok ? 'good' : 'bad', - 'content' => Minz_Translate::t($ok ? 'configuration_updated' : 'error_occurred') - ); - Minz_Session::_param('notification', $notif); } - Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true); + } + + /** + * This action displays the user management page. + */ + public function manageAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + Minz_View::prependTitle(_t('admin.user.title') . ' · '); + + // Get the correct current user. + $username = Minz_Request::param('u', Minz_Session::param('currentUser')); + if (!FreshRSS_UserDAO::exist($username)) { + $username = Minz_Session::param('currentUser'); + } + $this->view->current_user = $username; + + // Get information about the current user. + $entryDAO = FreshRSS_Factory::createEntryDao($this->view->current_user); + $this->view->nb_articles = $entryDAO->count(); + $this->view->size_user = $entryDAO->size(); } public function createAction() { - if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $db = Minz_Configuration::dataBase(); + if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) { + $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', $this->view->conf->language); - if (!in_array($new_user_language, $this->view->conf->availableLanguages())) { - $new_user_language = $this->view->conf->language; + $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) { - $ok &= (strcasecmp($new_user_name, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to alter the default user + $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 - $configPath = DATA_PATH . '/' . $new_user_name . '_user.php'; + $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 != '') { @@ -141,12 +148,13 @@ class FreshRSS_users_Controller extends Minz_ActionController { if (empty($new_user_email)) { $new_user_email = ''; } else { - $personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt'; + $personaFile = join_path(DATA_PATH, 'persona', $new_user_email . '.txt'); @unlink($personaFile); $ok &= (file_put_contents($personaFile, $new_user_name) !== false); } } if ($ok) { + mkdir(join_path(DATA_PATH, 'users', $new_user_name)); $config_array = array( 'language' => $new_user_language, 'passwordHash' => $passwordHash, @@ -162,42 +170,45 @@ class FreshRSS_users_Controller extends Minz_ActionController { $notif = array( 'type' => $ok ? 'good' : 'bad', - 'content' => Minz_Translate::t($ok ? 'user_created' : 'error_occurred', $new_user_name) + 'content' => _t('feedback.user.created' . (!$ok ? '.error' : ''), $new_user_name) ); Minz_Session::_param('notification', $notif); } - Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true); + + Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true); } public function deleteAction() { - if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $db = Minz_Configuration::dataBase(); + if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) { + $db = FreshRSS_Context::$system_conf->db; require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); $username = Minz_Request::param('username'); $ok = ctype_alnum($username); + $user_data = join_path(DATA_PATH, 'users', $username); if ($ok) { - $ok &= (strcasecmp($username, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to delete the default user + $default_user = FreshRSS_Context::$system_conf->default_user; + $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user } if ($ok) { - $configPath = DATA_PATH . '/' . $username . '_user.php'; - $ok &= file_exists($configPath); + $ok &= is_dir($user_data); } if ($ok) { $userDAO = new FreshRSS_UserDAO(); $ok &= $userDAO->deleteUser($username); - $ok &= unlink($configPath); + $ok &= recursive_unlink($user_data); //TODO: delete Persona file } invalidateHttpCache(); $notif = array( 'type' => $ok ? 'good' : 'bad', - 'content' => Minz_Translate::t($ok ? 'user_deleted' : 'error_occurred', $username) + 'content' => _t('feedback.user.deleted' . (!$ok ? '.error' : ''), $username) ); Minz_Session::_param('notification', $notif); } - Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true); + + Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true); } } diff --git a/sources/app/Exceptions/BadUrlException.php b/sources/app/Exceptions/BadUrlException.php index 7d1fe11..59574e1 100755 --- a/sources/app/Exceptions/BadUrlException.php +++ b/sources/app/Exceptions/BadUrlException.php @@ -1,6 +1,6 @@ accessControl(Minz_Session::param('currentUser', '')); - $this->loadParamsView(); + + // Register the configuration setter for the system configuration + $configuration_setter = new FreshRSS_ConfigurationSetter(); + $system_conf = Minz_Configuration::get('system'); + $system_conf->_configurationSetter($configuration_setter); + + // Load list of extensions and enable the "system" ones. + Minz_ExtensionManager::init(); + + // Auth has to be initialized before using currentUser session parameter + // because it's this part which create this parameter. + $this->initAuth(); + + // Then, register the user configuration and use the configuration setter + // created above. + $current_user = Minz_Session::param('currentUser', '_'); + Minz_Configuration::register('user', + join_path(USERS_PATH, $current_user, 'config.php'), + join_path(USERS_PATH, '_', 'config.default.php'), + $configuration_setter); + + // Finish to initialize the other FreshRSS / Minz components. + FreshRSS_Context::init(); + $this->initI18n(); + FreshRSS_Share::load(join_path(DATA_PATH, 'shares.php')); + $this->loadStylesAndScripts(); + $this->loadNotifications(); + // Enable extensions for the current (logged) user. + if (FreshRSS_Auth::hasAccess()) { + $ext_list = FreshRSS_Context::$user_conf->extensions_enabled; + Minz_ExtensionManager::enableByList($ext_list); + } + } + + private function initAuth() { + FreshRSS_Auth::init(); if (Minz_Request::isPost() && !is_referer_from_same_domain()) { - $loginOk = false; //Basic protection against XSRF attacks + // Basic protection against XSRF attacks + FreshRSS_Auth::removeAccess(); + $http_referer = empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']; Minz_Error::error( 403, - array('error' => array(Minz_Translate::t('access_denied') . ' [HTTP_REFERER=' . - htmlspecialchars(empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']) . ']')) + array('error' => array( + _t('access_denied'), + ' [HTTP_REFERER=' . htmlspecialchars($http_referer) . ']' + )) ); } - Minz_View::_param('loginOk', $loginOk); - $this->loadStylesAndScripts($loginOk); //TODO: Do not load that when not needed, e.g. some Ajax requests - $this->loadNotifications(); } - private static function getCredentialsFromLongTermCookie() { - $token = Minz_Session::getLongTermCookie('FreshRSS_login'); - if (!ctype_alnum($token)) { - return array(); - } - $tokenFile = DATA_PATH . '/tokens/' . $token . '.txt'; - $mtime = @filemtime($tokenFile); - if ($mtime + 2629744 < time()) { //1 month //TODO: Use a configuration instead - @unlink($tokenFile); - return array(); //Expired or token does not exist - } - $credentials = @file_get_contents($tokenFile); - return $credentials === false ? array() : explode("\t", $credentials, 2); + private function initI18n() { + Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); + Minz_Translate::init(FreshRSS_Context::$user_conf->language); } - private function accessControl($currentUser) { - if ($currentUser == '') { - switch (Minz_Configuration::authType()) { - case 'form': - $credentials = self::getCredentialsFromLongTermCookie(); - if (isset($credentials[1])) { - $currentUser = trim($credentials[0]); - Minz_Session::_param('passwordHash', trim($credentials[1])); - } - $loginOk = $currentUser != ''; - if (!$loginOk) { - $currentUser = Minz_Configuration::defaultUser(); - Minz_Session::_param('passwordHash'); - } - break; - case 'http_auth': - $currentUser = httpAuthUser(); - $loginOk = $currentUser != ''; - break; - case 'persona': - $loginOk = false; - $email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL); - if ($email != '') { //TODO: Remove redundancy with indexController - $personaFile = DATA_PATH . '/persona/' . $email . '.txt'; - if (($currentUser = @file_get_contents($personaFile)) !== false) { - $currentUser = trim($currentUser); - $loginOk = true; - } - } - if (!$loginOk) { - $currentUser = Minz_Configuration::defaultUser(); - } - break; - case 'none': - $currentUser = Minz_Configuration::defaultUser(); - $loginOk = true; - break; - default: - $currentUser = Minz_Configuration::defaultUser(); - $loginOk = false; - break; - } - } else { - $loginOk = true; - } - - if (!ctype_alnum($currentUser)) { - Minz_Session::_param('currentUser', ''); - die('Invalid username [' . $currentUser . ']!'); - } - - try { - $this->conf = new FreshRSS_Configuration($currentUser); - Minz_View::_param ('conf', $this->conf); - Minz_Session::_param('currentUser', $currentUser); - } catch (Minz_Exception $me) { - $loginOk = false; - try { - $this->conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser()); - Minz_Session::_param('currentUser', Minz_Configuration::defaultUser()); - Minz_View::_param('conf', $this->conf); - $notif = array( - 'type' => 'bad', - 'content' => 'Invalid configuration for user [' . $currentUser . ']!', - ); - Minz_Session::_param ('notification', $notif); - Minz_Log::record ($notif['content'] . ' ' . $me->getMessage(), Minz_Log::WARNING); - Minz_Session::_param('currentUser', ''); - } catch (Exception $e) { - die($e->getMessage()); - } - } - - if ($loginOk) { - switch (Minz_Configuration::authType()) { - case 'form': - $loginOk = Minz_Session::param('passwordHash') === $this->conf->passwordHash; - break; - case 'http_auth': - $loginOk = strcasecmp($currentUser, httpAuthUser()) === 0; - break; - case 'persona': - $loginOk = strcasecmp(Minz_Session::param('mail'), $this->conf->mail_login) === 0; - break; - case 'none': - $loginOk = true; - break; - default: - $loginOk = false; - break; - } - } - return $loginOk; - } - - private function loadParamsView () { - Minz_Session::_param ('language', $this->conf->language); - Minz_Translate::init(); - $output = Minz_Request::param ('output', ''); - if (($output === '') || ($output !== 'normal' && $output !== 'rss' && $output !== 'reader' && $output !== 'global')) { - $output = $this->conf->view_mode; - Minz_Request::_param ('output', $output); - } - } - - private function loadStylesAndScripts($loginOk) { - $theme = FreshRSS_Themes::load($this->conf->theme); + private function loadStylesAndScripts() { + $theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme); if ($theme) { foreach($theme['files'] as $file) { if ($file[0] === '_') { @@ -157,26 +96,24 @@ class FreshRSS extends Minz_FrontController { } } - switch (Minz_Configuration::authType()) { - case 'form': - if (!$loginOk) { - Minz_View::appendScript(Minz_Url::display ('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'))); - } - break; - case 'persona': - Minz_View::appendScript('https://login.persona.org/include.js'); - break; - } Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js'))); Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js'))); + + if (FreshRSS_Context::$system_conf->auth_type === 'persona') { + // TODO move it in a plugin + // Needed for login AND logout with Persona. + Minz_View::appendScript('https://login.persona.org/include.js'); + $file_mtime = @filemtime(PUBLIC_PATH . '/scripts/persona.js'); + Minz_View::appendScript(Minz_Url::display('/scripts/persona.js?' . $file_mtime)); + } } - private function loadNotifications () { - $notif = Minz_Session::param ('notification'); + private function loadNotifications() { + $notif = Minz_Session::param('notification'); if ($notif) { - Minz_View::_param ('notification', $notif); - Minz_Session::_param ('notification'); + Minz_View::_param('notification', $notif); + Minz_Session::_param('notification'); } } } diff --git a/sources/app/Models/Auth.php b/sources/app/Models/Auth.php new file mode 100755 index 0000000..4e7a719 --- /dev/null +++ b/sources/app/Models/Auth.php @@ -0,0 +1,254 @@ +default_user; + Minz_Session::_param('currentUser', $current_user); + } + + if (self::$login_ok) { + self::giveAccess(); + } elseif (self::accessControl()) { + self::giveAccess(); + FreshRSS_UserDAO::touch($current_user); + } else { + // Be sure all accesses are removed! + self::removeAccess(); + } + } + + /** + * This method checks if user is allowed to connect. + * + * Required session parameters are also set in this method (such as + * currentUser). + * + * @return boolean true if user can be connected, false else. + */ + private static function accessControl() { + $conf = Minz_Configuration::get('system'); + $auth_type = $conf->auth_type; + switch ($auth_type) { + case 'form': + $credentials = FreshRSS_FormAuth::getCredentialsFromCookie(); + $current_user = ''; + if (isset($credentials[1])) { + $current_user = trim($credentials[0]); + Minz_Session::_param('currentUser', $current_user); + Minz_Session::_param('passwordHash', trim($credentials[1])); + } + return $current_user != ''; + case 'http_auth': + $current_user = httpAuthUser(); + $login_ok = $current_user != ''; + if ($login_ok) { + Minz_Session::_param('currentUser', $current_user); + } + return $login_ok; + case 'persona': + $email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL); + $persona_file = DATA_PATH . '/persona/' . $email . '.txt'; + if (($current_user = @file_get_contents($persona_file)) !== false) { + $current_user = trim($current_user); + Minz_Session::_param('currentUser', $current_user); + Minz_Session::_param('mail', $email); + return true; + } + return false; + case 'none': + return true; + default: + // TODO load extension + return false; + } + } + + /** + * Gives access to the current user. + */ + public static function giveAccess() { + $current_user = Minz_Session::param('currentUser'); + $user_conf = get_user_configuration($current_user); + $system_conf = Minz_Configuration::get('system'); + + switch ($system_conf->auth_type) { + case 'form': + self::$login_ok = Minz_Session::param('passwordHash') === $user_conf->passwordHash; + break; + case 'http_auth': + self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0; + break; + case 'persona': + self::$login_ok = strcasecmp(Minz_Session::param('mail'), $user_conf->mail_login) === 0; + break; + case 'none': + self::$login_ok = true; + break; + default: + // TODO: extensions + self::$login_ok = false; + } + + Minz_Session::_param('loginOk', self::$login_ok); + } + + /** + * Returns if current user has access to the given scope. + * + * @param string $scope general (default) or admin + * @return boolean true if user has corresponding access, false else. + */ + public static function hasAccess($scope = 'general') { + $conf = Minz_Configuration::get('system'); + $default_user = $conf->default_user; + $ok = self::$login_ok; + switch ($scope) { + case 'general': + break; + case 'admin': + $ok &= Minz_Session::param('currentUser') === $default_user; + break; + default: + $ok = false; + } + return $ok; + } + + /** + * Removes all accesses for the current user. + */ + public static function removeAccess() { + Minz_Session::_param('loginOk'); + self::$login_ok = false; + $conf = Minz_Configuration::get('system'); + Minz_Session::_param('currentUser', $conf->default_user); + + switch ($conf->auth_type) { + case 'form': + Minz_Session::_param('passwordHash'); + FreshRSS_FormAuth::deleteCookie(); + break; + case 'persona': + Minz_Session::_param('mail'); + break; + case 'http_auth': + case 'none': + // Nothing to do... + break; + default: + // TODO: extensions + } + } + + /** + * Return if authentication is enabled on this instance of FRSS. + */ + public static function accessNeedsLogin() { + $conf = Minz_Configuration::get('system'); + $auth_type = $conf->auth_type; + return $auth_type !== 'none'; + } + + /** + * Return if authentication requires a PHP action. + */ + public static function accessNeedsAction() { + $conf = Minz_Configuration::get('system'); + $auth_type = $conf->auth_type; + return $auth_type === 'form' || $auth_type === 'persona'; + } +} + + +class FreshRSS_FormAuth { + public static function checkCredentials($username, $hash, $nonce, $challenge) { + if (!ctype_alnum($username) || + !ctype_graph($challenge) || + !ctype_alnum($nonce)) { + Minz_Log::debug('Invalid credential parameters:' . + ' user=' . $username . + ' challenge=' . $challenge . + ' nonce=' . $nonce); + return false; + } + + if (!function_exists('password_verify')) { + include_once(LIB_PATH . '/password_compat.php'); + } + + return password_verify($nonce . $hash, $challenge); + } + + public static function getCredentialsFromCookie() { + $token = Minz_Session::getLongTermCookie('FreshRSS_login'); + if (!ctype_alnum($token)) { + return array(); + } + + $token_file = DATA_PATH . '/tokens/' . $token . '.txt'; + $mtime = @filemtime($token_file); + if ($mtime + 2629744 < time()) { + // Token has expired (> 1 month) or does not exist. + // TODO: 1 month -> use a configuration instead + @unlink($token_file); + return array(); + } + + $credentials = @file_get_contents($token_file); + return $credentials === false ? array() : explode("\t", $credentials, 2); + } + + public static function makeCookie($username, $password_hash) { + do { + $conf = Minz_Configuration::get('system'); + $token = sha1($conf->salt . $username . uniqid(mt_rand(), true)); + $token_file = DATA_PATH . '/tokens/' . $token . '.txt'; + } while (file_exists($token_file)); + + if (@file_put_contents($token_file, $username . "\t" . $password_hash) === false) { + return false; + } + + $expire = time() + 2629744; //1 month //TODO: Use a configuration instead + Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire); + return $token; + } + + public static function deleteCookie() { + $token = Minz_Session::getLongTermCookie('FreshRSS_login'); + Minz_Session::deleteLongTermCookie('FreshRSS_login'); + if (ctype_alnum($token)) { + @unlink(DATA_PATH . '/tokens/' . $token . '.txt'); + } + + if (rand(0, 10) === 1) { + self::purgeTokens(); + } + } + + public static function purgeTokens() { + $oldest = time() - 2629744; // 1 month // TODO: Use a configuration instead + foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) { + // $extension = $file_info->getExtension(); doesn't work in PHP < 5.3.7 + $extension = pathinfo($file_info->getFilename(), PATHINFO_EXTENSION); + if ($extension === 'txt' && $file_info->getMTime() < $oldest) { + @unlink($file_info->getPathname()); + } + } + } +} diff --git a/sources/app/Models/Category.php b/sources/app/Models/Category.php index 0a0dbd3..37cb44d 100755 --- a/sources/app/Models/Category.php +++ b/sources/app/Models/Category.php @@ -7,65 +7,65 @@ class FreshRSS_Category extends Minz_Model { private $nbNotRead = -1; private $feeds = null; - public function __construct ($name = '', $feeds = null) { - $this->_name ($name); - if (isset ($feeds)) { - $this->_feeds ($feeds); + public function __construct($name = '', $feeds = null) { + $this->_name($name); + if (isset($feeds)) { + $this->_feeds($feeds); $this->nbFeed = 0; $this->nbNotRead = 0; foreach ($feeds as $feed) { $this->nbFeed++; - $this->nbNotRead += $feed->nbNotRead (); + $this->nbNotRead += $feed->nbNotRead(); } } } - public function id () { + public function id() { return $this->id; } - public function name () { + public function name() { return $this->name; } - public function nbFeed () { + public function nbFeed() { if ($this->nbFeed < 0) { - $catDAO = new FreshRSS_CategoryDAO (); - $this->nbFeed = $catDAO->countFeed ($this->id ()); + $catDAO = new FreshRSS_CategoryDAO(); + $this->nbFeed = $catDAO->countFeed($this->id()); } return $this->nbFeed; } - public function nbNotRead () { + public function nbNotRead() { if ($this->nbNotRead < 0) { - $catDAO = new FreshRSS_CategoryDAO (); - $this->nbNotRead = $catDAO->countNotRead ($this->id ()); + $catDAO = new FreshRSS_CategoryDAO(); + $this->nbNotRead = $catDAO->countNotRead($this->id()); } return $this->nbNotRead; } - public function feeds () { + public function feeds() { if ($this->feeds === null) { $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->feeds = $feedDAO->listByCategory ($this->id ()); + $this->feeds = $feedDAO->listByCategory($this->id()); $this->nbFeed = 0; $this->nbNotRead = 0; foreach ($this->feeds as $feed) { $this->nbFeed++; - $this->nbNotRead += $feed->nbNotRead (); + $this->nbNotRead += $feed->nbNotRead(); } } return $this->feeds; } - public function _id ($value) { + public function _id($value) { $this->id = $value; } - public function _name ($value) { - $this->name = $value; + public function _name($value) { + $this->name = substr(trim($value), 0, 255); } - public function _feeds ($values) { - if (!is_array ($values)) { - $values = array ($values); + public function _feeds($values) { + if (!is_array($values)) { + $values = array($values); } $this->feeds = $values; diff --git a/sources/app/Models/CategoryDAO.php b/sources/app/Models/CategoryDAO.php index f11f87f..27a5585 100755 --- a/sources/app/Models/CategoryDAO.php +++ b/sources/app/Models/CategoryDAO.php @@ -1,19 +1,19 @@ prefix . 'category` (name) VALUES(?)'; - $stm = $this->bd->prepare ($sql); + public function addCategory($valuesTmp) { + $sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)'; + $stm = $this->bd->prepare($sql); - $values = array ( + $values = array( substr($valuesTmp['name'], 0, 255), ); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $this->bd->lastInsertId(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error addCategory: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error addCategory: ' . $info[2] ); return false; } } @@ -31,73 +31,73 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { return $cat->id(); } - public function updateCategory ($id, $valuesTmp) { + public function updateCategory($id, $valuesTmp) { $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ( + $values = array( $valuesTmp['name'], $id ); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error updateCategory: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error updateCategory: ' . $info[2]); return false; } } - public function deleteCategory ($id) { + public function deleteCategory($id) { $sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($id); + $values = array($id); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error deleteCategory: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error deleteCategory: ' . $info[2]); return false; } } - public function searchById ($id) { + public function searchById($id) { $sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($id); + $values = array($id); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); - $cat = self::daoToCategory ($res); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $cat = self::daoToCategory($res); - if (isset ($cat[0])) { + if (isset($cat[0])) { return $cat[0]; } else { return null; } } - public function searchByName ($name) { + public function searchByName($name) { $sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE name=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($name); + $values = array($name); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); - $cat = self::daoToCategory ($res); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $cat = self::daoToCategory($res); - if (isset ($cat[0])) { + if (isset($cat[0])) { return $cat[0]; } else { return null; } } - public function listCategories ($prePopulateFeeds = true, $details = false) { + 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 ') @@ -105,80 +105,80 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id ' . 'GROUP BY f.id ' . 'ORDER BY c.name, f.name'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); - return self::daoToCategoryPrepopulated ($stm->fetchAll (PDO::FETCH_ASSOC)); + $stm = $this->bd->prepare($sql); + $stm->execute(); + return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC)); } else { $sql = 'SELECT * FROM `' . $this->prefix . 'category` ORDER BY name'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); - return self::daoToCategory ($stm->fetchAll (PDO::FETCH_ASSOC)); + $stm = $this->bd->prepare($sql); + $stm->execute(); + return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC)); } } - public function getDefault () { + public function getDefault() { $sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=1'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $stm->execute (); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); - $cat = self::daoToCategory ($res); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $cat = self::daoToCategory($res); - if (isset ($cat[0])) { + if (isset($cat[0])) { return $cat[0]; } else { return false; } } - public function checkDefault () { - $def_cat = $this->searchById (1); + public function checkDefault() { + $def_cat = $this->searchById(1); if ($def_cat == null) { - $cat = new FreshRSS_Category (Minz_Translate::t ('default_category')); - $cat->_id (1); + $cat = new FreshRSS_Category(_t('gen.short.default_category')); + $cat->_id(1); - $values = array ( - 'id' => $cat->id (), - 'name' => $cat->name (), + $values = array( + 'id' => $cat->id(), + 'name' => $cat->name(), ); - $this->addCategory ($values); + $this->addCategory($values); } } - public function count () { + public function count() { $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'category`'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); return $res[0]['count']; } - public function countFeed ($id) { + public function countFeed($id) { $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'feed` WHERE category=?'; - $stm = $this->bd->prepare ($sql); - $values = array ($id); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); return $res[0]['count']; } - public function countNotRead ($id) { + public function countNotRead($id) { $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE category=? AND e.is_read=0'; - $stm = $this->bd->prepare ($sql); - $values = array ($id); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); return $res[0]['count']; } public static function findFeed($categories, $feed_id) { foreach ($categories as $category) { - foreach ($category->feeds () as $feed) { - if ($feed->id () === $feed_id) { + foreach ($category->feeds() as $feed) { + if ($feed->id() === $feed_id) { return $feed; } } @@ -189,8 +189,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { public static function CountUnreads($categories, $minPriority = 0) { $n = 0; foreach ($categories as $category) { - foreach ($category->feeds () as $feed) { - if ($feed->priority () >= $minPriority) { + foreach ($category->feeds() as $feed) { + if ($feed->priority() >= $minPriority) { $n += $feed->nbNotRead(); } } @@ -198,11 +198,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { return $n; } - public static function daoToCategoryPrepopulated ($listDAO) { - $list = array (); + public static function daoToCategoryPrepopulated($listDAO) { + $list = array(); - if (!is_array ($listDAO)) { - $listDAO = array ($listDAO); + if (!is_array($listDAO)) { + $listDAO = array($listDAO); } $previousLine = null; @@ -210,11 +210,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { 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 ( + $cat = new FreshRSS_Category( $previousLine['c_name'], - FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id']) + FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id']) ); - $cat->_id ($previousLine['c_id']); + $cat->_id($previousLine['c_id']); $list[$previousLine['c_id']] = $cat; $feedsDao = array(); //Prepare for next category @@ -226,29 +226,29 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { // add the last category if ($previousLine != null) { - $cat = new FreshRSS_Category ( + $cat = new FreshRSS_Category( $previousLine['c_name'], - FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id']) + FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id']) ); - $cat->_id ($previousLine['c_id']); + $cat->_id($previousLine['c_id']); $list[$previousLine['c_id']] = $cat; } return $list; } - public static function daoToCategory ($listDAO) { - $list = array (); + public static function daoToCategory($listDAO) { + $list = array(); - if (!is_array ($listDAO)) { - $listDAO = array ($listDAO); + if (!is_array($listDAO)) { + $listDAO = array($listDAO); } foreach ($listDAO as $key => $dao) { - $cat = new FreshRSS_Category ( + $cat = new FreshRSS_Category( $dao['name'] ); - $cat->_id ($dao['id']); + $cat->_id($dao['id']); $list[$key] = $cat; } diff --git a/sources/app/Models/Configuration.php b/sources/app/Models/Configuration.php deleted file mode 100755 index 95f8197..0000000 --- a/sources/app/Models/Configuration.php +++ /dev/null @@ -1,335 +0,0 @@ - 'en', - 'old_entries' => 3, - 'keep_history_default' => 0, - 'ttl_default' => 3600, - 'mail_login' => '', - 'token' => '', - 'passwordHash' => '', //CRYPT_BLOWFISH - 'apiPasswordHash' => '', //CRYPT_BLOWFISH - 'posts_per_page' => 20, - 'view_mode' => 'normal', - 'default_view' => FreshRSS_Entry::STATE_NOT_READ, - 'auto_load_more' => true, - 'display_posts' => false, - 'display_categories' => false, - 'hide_read_feeds' => true, - 'onread_jump_next' => true, - 'lazyload' => true, - 'sticky_post' => true, - 'reading_confirm' => false, - 'sort_order' => 'DESC', - 'anon_access' => false, - 'mark_when' => array( - 'article' => true, - 'site' => true, - 'scroll' => false, - 'reception' => false, - ), - 'theme' => 'Origine', - 'content_width' => 'thin', - 'shortcuts' => array( - 'mark_read' => 'r', - 'mark_favorite' => 'f', - 'go_website' => 'space', - 'next_entry' => 'j', - 'prev_entry' => 'k', - 'first_entry' => 'home', - 'last_entry' => 'end', - 'collapse_entry' => 'c', - 'load_more' => 'm', - 'auto_share' => 's', - 'focus_search' => 'a', - 'user_filter' => 'u', - 'help' => 'f1', - ), - 'topline_read' => true, - 'topline_favorite' => true, - 'topline_date' => true, - 'topline_link' => true, - 'bottomline_read' => true, - 'bottomline_favorite' => true, - 'bottomline_sharing' => true, - 'bottomline_tags' => true, - 'bottomline_date' => true, - 'bottomline_link' => true, - 'sharing' => array(), - 'queries' => array(), - 'html5_notif_timeout' => 0, - ); - - private $available_languages = array( - 'en' => 'English', - 'fr' => 'Français', - ); - - private $shares; - - public function __construct($user) { - $this->filename = DATA_PATH . DIRECTORY_SEPARATOR . $user . '_user.php'; - - $data = @include($this->filename); - if (!is_array($data)) { - throw new Minz_PermissionDeniedException($this->filename); - } - - foreach ($data as $key => $value) { - if (isset($this->data[$key])) { - $function = '_' . $key; - $this->$function($value); - } - } - $this->data['user'] = $user; - - $this->shares = DATA_PATH . DIRECTORY_SEPARATOR . 'shares.php'; - - $shares = @include($this->shares); - if (!is_array($shares)) { - throw new Minz_PermissionDeniedException($this->shares); - } - - $this->data['shares'] = $shares; - } - - public function save() { - @rename($this->filename, $this->filename . '.bak.php'); - unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration - if (file_put_contents($this->filename, "data, true) . ';', LOCK_EX) === false) { - throw new Minz_PermissionDeniedException($this->filename); - } - if (function_exists('opcache_invalidate')) { - opcache_invalidate($this->filename); //Clear PHP 5.5+ cache for include - } - invalidateHttpCache(); - return true; - } - - public function __get($name) { - if (array_key_exists($name, $this->data)) { - return $this->data[$name]; - } else { - $trace = debug_backtrace(); - trigger_error('Undefined FreshRSS_Configuration->' . $name . 'in ' . $trace[0]['file'] . ' line ' . $trace[0]['line'], E_USER_NOTICE); //TODO: Use Minz exceptions - return null; - } - } - - public function availableLanguages() { - return $this->available_languages; - } - - public function remove_query_by_get($get) { - $final_queries = array(); - foreach ($this->queries as $key => $query) { - if (empty($query['get']) || $query['get'] !== $get) { - $final_queries[$key] = $query; - } - } - $this->_queries($final_queries); - } - - public function _language($value) { - if (!isset($this->available_languages[$value])) { - $value = 'en'; - } - $this->data['language'] = $value; - } - public function _posts_per_page ($value) { - $value = intval($value); - $this->data['posts_per_page'] = $value > 0 ? $value : 10; - } - public function _view_mode ($value) { - if ($value === 'global' || $value === 'reader') { - $this->data['view_mode'] = $value; - } else { - $this->data['view_mode'] = 'normal'; - } - } - public function _default_view ($value) { - switch ($value) { - case FreshRSS_Entry::STATE_ALL: - // left blank on purpose - case FreshRSS_Entry::STATE_NOT_READ: - // left blank on purpose - case FreshRSS_Entry::STATE_STRICT + FreshRSS_Entry::STATE_NOT_READ: - $this->data['default_view'] = $value; - break; - default: - $this->data['default_view'] = FreshRSS_Entry::STATE_ALL; - break; - } - } - public function _display_posts ($value) { - $this->data['display_posts'] = ((bool)$value) && $value !== 'no'; - } - public function _display_categories ($value) { - $this->data['display_categories'] = ((bool)$value) && $value !== 'no'; - } - public function _hide_read_feeds($value) { - $this->data['hide_read_feeds'] = (bool)$value; - } - public function _onread_jump_next ($value) { - $this->data['onread_jump_next'] = ((bool)$value) && $value !== 'no'; - } - public function _lazyload ($value) { - $this->data['lazyload'] = ((bool)$value) && $value !== 'no'; - } - public function _sticky_post($value) { - $this->data['sticky_post'] = ((bool)$value) && $value !== 'no'; - } - public function _reading_confirm($value) { - $this->data['reading_confirm'] = ((bool)$value) && $value !== 'no'; - } - public function _sort_order ($value) { - $this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC'; - } - public function _old_entries($value) { - $value = intval($value); - $this->data['old_entries'] = $value > 0 ? $value : 3; - } - public function _keep_history_default($value) { - $value = intval($value); - $this->data['keep_history_default'] = $value >= -1 ? $value : 0; - } - public function _ttl_default($value) { - $value = intval($value); - $this->data['ttl_default'] = $value >= -1 ? $value : 3600; - } - public function _shortcuts ($values) { - foreach ($values as $key => $value) { - if (isset($this->data['shortcuts'][$key])) { - $this->data['shortcuts'][$key] = $value; - } - } - } - public function _passwordHash ($value) { - $this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; - } - public function _apiPasswordHash ($value) { - $this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; - } - public function _mail_login ($value) { - $value = filter_var($value, FILTER_VALIDATE_EMAIL); - if ($value) { - $this->data['mail_login'] = $value; - } else { - $this->data['mail_login'] = ''; - } - } - public function _anon_access ($value) { - $this->data['anon_access'] = ((bool)$value) && $value !== 'no'; - } - public function _mark_when ($values) { - foreach ($values as $key => $value) { - if (isset($this->data['mark_when'][$key])) { - $this->data['mark_when'][$key] = ((bool)$value) && $value !== 'no'; - } - } - } - public function _sharing ($values) { - $this->data['sharing'] = array(); - $unique = array(); - foreach ($values as $value) { - if (!is_array($value)) { - continue; - } - - // Verify URL and add default value when needed - if (isset($value['url'])) { - $is_url = ( - filter_var ($value['url'], FILTER_VALIDATE_URL) || - (version_compare(PHP_VERSION, '5.3.3', '<') && - (strpos($value, '-') > 0) && - ($value === filter_var($value, FILTER_SANITIZE_URL))) - ); //PHP bug #51192 - if (!$is_url) { - continue; - } - } else { - $value['url'] = null; - } - - // Add a default name - if (empty($value['name'])) { - $value['name'] = $value['type']; - } - - $json_value = json_encode($value); - if (!in_array($json_value, $unique)) { - $unique[] = $json_value; - $this->data['sharing'][] = $value; - } - } - } - public function _queries ($values) { - $this->data['queries'] = array(); - foreach ($values as $value) { - $value = array_filter($value); - $params = $value; - unset($params['name']); - unset($params['url']); - $value['url'] = Minz_Url::display(array('params' => $params)); - - $this->data['queries'][] = $value; - } - } - public function _theme($value) { - $this->data['theme'] = $value; - } - public function _content_width($value) { - if ($value === 'medium' || - $value === 'large' || - $value === 'no_limit') { - $this->data['content_width'] = $value; - } else { - $this->data['content_width'] = 'thin'; - } - } - - public function _html5_notif_timeout ($value) { - $value = intval($value); - $this->data['html5_notif_timeout'] = $value >= 0 ? $value : 0; - } - - public function _token($value) { - $this->data['token'] = $value; - } - public function _auto_load_more($value) { - $this->data['auto_load_more'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_read($value) { - $this->data['topline_read'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_favorite($value) { - $this->data['topline_favorite'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_date($value) { - $this->data['topline_date'] = ((bool)$value) && $value !== 'no'; - } - public function _topline_link($value) { - $this->data['topline_link'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_read($value) { - $this->data['bottomline_read'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_favorite($value) { - $this->data['bottomline_favorite'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_sharing($value) { - $this->data['bottomline_sharing'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_tags($value) { - $this->data['bottomline_tags'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_date($value) { - $this->data['bottomline_date'] = ((bool)$value) && $value !== 'no'; - } - public function _bottomline_link($value) { - $this->data['bottomline_link'] = ((bool)$value) && $value !== 'no'; - } -} diff --git a/sources/app/Models/ConfigurationSetter.php b/sources/app/Models/ConfigurationSetter.php new file mode 100755 index 0000000..eeb1f2f --- /dev/null +++ b/sources/app/Models/ConfigurationSetter.php @@ -0,0 +1,374 @@ += 60) ? $value : ''; + } + + private function _content_width(&$data, $value) { + $value = strtolower($value); + if (!in_array($value, array('thin', 'medium', 'large', 'no_limit'))) { + $value = 'thin'; + } + + $data['content_width'] = $value; + } + + private function _default_state(&$data, $value) { + $data['default_state'] = (int)$value; + } + + private function _default_view(&$data, $value) { + switch ($value) { + case 'all': + $data['default_view'] = $value; + $data['default_state'] = (FreshRSS_Entry::STATE_READ + + FreshRSS_Entry::STATE_NOT_READ); + break; + case 'adaptive': + case 'unread': + default: + $data['default_view'] = $value; + $data['default_state'] = FreshRSS_Entry::STATE_NOT_READ; + } + } + + // It works for system config too! + private function _extensions_enabled(&$data, $value) { + if (!is_array($value)) { + $value = array($value); + } + $data['extensions_enabled'] = $value; + } + + private function _html5_notif_timeout(&$data, $value) { + $value = intval($value); + $data['html5_notif_timeout'] = $value >= 0 ? $value : 0; + } + + private function _keep_history_default(&$data, $value) { + $value = intval($value); + $data['keep_history_default'] = $value >= -1 ? $value : 0; + } + + // It works for system config too! + private function _language(&$data, $value) { + $value = strtolower($value); + $languages = Minz_Translate::availableLanguages(); + if (!in_array($value, $languages)) { + $value = 'en'; + } + $data['language'] = $value; + } + + private function _mail_login(&$data, $value) { + $value = filter_var($value, FILTER_VALIDATE_EMAIL); + $data['mail_login'] = $value ? $value : ''; + } + + private function _old_entries(&$data, $value) { + $value = intval($value); + $data['old_entries'] = $value > 0 ? $value : 3; + } + + private function _passwordHash(&$data, $value) { + $data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; + } + + private function _posts_per_page(&$data, $value) { + $value = intval($value); + $data['posts_per_page'] = $value > 0 ? $value : 10; + } + + private function _queries(&$data, $values) { + $data['queries'] = array(); + foreach ($values as $value) { + $value = array_filter($value); + $params = $value; + unset($params['name']); + unset($params['url']); + $value['url'] = Minz_Url::display(array('params' => $params)); + $data['queries'][] = $value; + } + } + + private function _sharing(&$data, $values) { + $data['sharing'] = array(); + foreach ($values as $value) { + if (!is_array($value)) { + continue; + } + + // Verify URL and add default value when needed + if (isset($value['url'])) { + $is_url = ( + filter_var($value['url'], FILTER_VALIDATE_URL) || + (version_compare(PHP_VERSION, '5.3.3', '<') && + (strpos($value, '-') > 0) && + ($value === filter_var($value, FILTER_SANITIZE_URL))) + ); //PHP bug #51192 + if (!$is_url) { + continue; + } + } else { + $value['url'] = null; + } + + $data['sharing'][] = $value; + } + } + + private function _shortcuts(&$data, $values) { + if (!is_array($values)) { + return; + } + + $data['shortcuts'] = $values; + } + + private function _sort_order(&$data, $value) { + $data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC'; + } + + private function _ttl_default(&$data, $value) { + $value = intval($value); + $data['ttl_default'] = $value >= -1 ? $value : 3600; + } + + private function _view_mode(&$data, $value) { + $value = strtolower($value); + if (!in_array($value, array('global', 'normal', 'reader'))) { + $value = 'normal'; + } + $data['view_mode'] = $value; + } + + /** + * A list of boolean setters. + */ + private function _anon_access(&$data, $value) { + $data['anon_access'] = $this->handleBool($value); + } + + private function _auto_load_more(&$data, $value) { + $data['auto_load_more'] = $this->handleBool($value); + } + + private function _auto_remove_article(&$data, $value) { + $data['auto_remove_article'] = $this->handleBool($value); + } + + private function _display_categories(&$data, $value) { + $data['display_categories'] = $this->handleBool($value); + } + + private function _display_posts(&$data, $value) { + $data['display_posts'] = $this->handleBool($value); + } + + private function _hide_read_feeds(&$data, $value) { + $data['hide_read_feeds'] = $this->handleBool($value); + } + + private function _lazyload(&$data, $value) { + $data['lazyload'] = $this->handleBool($value); + } + + private function _mark_when(&$data, $values) { + foreach ($values as $key => $value) { + $data['mark_when'][$key] = $this->handleBool($value); + } + } + + private function _onread_jump_next(&$data, $value) { + $data['onread_jump_next'] = $this->handleBool($value); + } + + private function _reading_confirm(&$data, $value) { + $data['reading_confirm'] = $this->handleBool($value); + } + + private function _sticky_post(&$data, $value) { + $data['sticky_post'] = $this->handleBool($value); + } + + private function _bottomline_date(&$data, $value) { + $data['bottomline_date'] = $this->handleBool($value); + } + private function _bottomline_favorite(&$data, $value) { + $data['bottomline_favorite'] = $this->handleBool($value); + } + private function _bottomline_link(&$data, $value) { + $data['bottomline_link'] = $this->handleBool($value); + } + private function _bottomline_read(&$data, $value) { + $data['bottomline_read'] = $this->handleBool($value); + } + private function _bottomline_sharing(&$data, $value) { + $data['bottomline_sharing'] = $this->handleBool($value); + } + private function _bottomline_tags(&$data, $value) { + $data['bottomline_tags'] = $this->handleBool($value); + } + + private function _topline_date(&$data, $value) { + $data['topline_date'] = $this->handleBool($value); + } + private function _topline_favorite(&$data, $value) { + $data['topline_favorite'] = $this->handleBool($value); + } + private function _topline_link(&$data, $value) { + $data['topline_link'] = $this->handleBool($value); + } + private function _topline_read(&$data, $value) { + $data['topline_read'] = $this->handleBool($value); + } + + /** + * The (not so long) list of setters for system configuration. + */ + private function _allow_anonymous(&$data, $value) { + $data['allow_anonymous'] = $this->handleBool($value) && FreshRSS_Auth::accessNeedsAction(); + } + + private function _allow_anonymous_refresh(&$data, $value) { + $data['allow_anonymous_refresh'] = $this->handleBool($value) && $data['allow_anonymous']; + } + + private function _api_enabled(&$data, $value) { + $data['api_enabled'] = $this->handleBool($value); + } + + private function _auth_type(&$data, $value) { + $value = strtolower($value); + if (!in_array($value, array('form', 'http_auth', 'persona', 'none'))) { + $value = 'none'; + } + $data['auth_type'] = $value; + $this->_allow_anonymous($data, $data['allow_anonymous']); + } + + private function _db(&$data, $value) { + if (!isset($value['type'])) { + return; + } + + switch ($value['type']) { + case 'mysql': + if (empty($value['host']) || + empty($value['user']) || + empty($value['base']) || + !isset($value['password'])) { + return; + } + + $data['db']['type'] = $value['type']; + $data['db']['host'] = $value['host']; + $data['db']['user'] = $value['user']; + $data['db']['base'] = $value['base']; + $data['db']['password'] = $value['password']; + $data['db']['prefix'] = isset($value['prefix']) ? $value['prefix'] : ''; + break; + case 'sqlite': + $data['db']['type'] = $value['type']; + $data['db']['host'] = ''; + $data['db']['user'] = ''; + $data['db']['base'] = ''; + $data['db']['password'] = ''; + $data['db']['prefix'] = ''; + break; + default: + return; + } + } + + private function _default_user(&$data, $value) { + $user_list = listUsers(); + if (in_array($value, $user_list)) { + $data['default_user'] = $value; + } + } + + private function _environment(&$data, $value) { + $value = strtolower($value); + if (!in_array($value, array('silent', 'development', 'production'))) { + $value = 'production'; + } + $data['environment'] = $value; + } + + private function _limits(&$data, $values) { + $max_small_int = 16384; + $limits_keys = array( + 'cache_duration' => array( + 'min' => 0, + ), + 'timeout' => array( + 'min' => 0, + ), + 'max_inactivity' => array( + 'min' => 0, + ), + 'max_feeds' => array( + 'min' => 0, + 'max' => $max_small_int, + ), + 'max_categories' => array( + 'min' => 0, + 'max' => $max_small_int, + ), + ); + + foreach ($values as $key => $value) { + if (!isset($limits_keys[$key])) { + continue; + } + + $limits = $limits_keys[$key]; + if ( + (!isset($limits['min']) || $value > $limits['min']) && + (!isset($limits['max']) || $value < $limits['max']) + ) { + $data['limits'][$key] = $value; + } + } + } + + private function _unsafe_autologin_enabled(&$data, $value) { + $data['unsafe_autologin_enabled'] = $this->handleBool($value); + } +} diff --git a/sources/app/Models/Context.php b/sources/app/Models/Context.php new file mode 100755 index 0000000..1c770c7 --- /dev/null +++ b/sources/app/Models/Context.php @@ -0,0 +1,304 @@ + 0, + 'read' => 0, + 'unread' => 0, + ); + + public static $get_unread = 0; + public static $current_get = array( + 'all' => false, + 'starred' => false, + 'feed' => false, + 'category' => false, + ); + public static $next_get = 'a'; + + public static $state = 0; + public static $order = 'DESC'; + public static $number = 0; + public static $search = ''; + public static $first_id = ''; + public static $next_id = ''; + public static $id_max = ''; + + /** + * Initialize the context. + * + * Set the correct configurations and $categories variables. + */ + public static function init() { + // Init configuration. + self::$system_conf = Minz_Configuration::get('system'); + self::$user_conf = Minz_Configuration::get('user'); + + $catDAO = new FreshRSS_CategoryDAO(); + self::$categories = $catDAO->listCategories(); + } + + /** + * Returns if the current state includes $state parameter. + */ + public static function isStateEnabled($state) { + return self::$state & $state; + } + + /** + * Returns the current state with or without $state parameter. + */ + public static function getRevertState($state) { + if (self::$state & $state) { + return self::$state & ~$state; + } else { + return self::$state | $state; + } + } + + /** + * Return the current get as a string or an array. + * + * If $array is true, the first item of the returned value is 'f' or 'c' and + * the second is the id. + */ + public static function currentGet($array = false) { + if (self::$current_get['all']) { + return 'a'; + } elseif (self::$current_get['starred']) { + return 's'; + } elseif (self::$current_get['feed']) { + if ($array) { + return array('f', self::$current_get['feed']); + } else { + return 'f_' . self::$current_get['feed']; + } + } elseif (self::$current_get['category']) { + if ($array) { + return array('c', self::$current_get['category']); + } else { + return 'c_' . self::$current_get['category']; + } + } + } + + /** + * Return true if $get parameter correspond to the $current_get attribute. + */ + public static function isCurrentGet($get) { + $type = $get[0]; + $id = substr($get, 2); + + switch($type) { + case 'a': + return self::$current_get['all']; + case 's': + return self::$current_get['starred']; + case 'f': + return self::$current_get['feed'] == $id; + case 'c': + return self::$current_get['category'] == $id; + default: + return false; + } + } + + /** + * Set the current $get attribute. + * + * Valid $get parameter are: + * - a + * - s + * - f_ + * - c_ + * + * $name and $get_unread attributes are also updated as $next_get + * Raise an exception if id or $get is invalid. + */ + public static function _get($get) { + $type = $get[0]; + $id = substr($get, 2); + $nb_unread = 0; + + switch($type) { + case 'a': + self::$current_get['all'] = true; + self::$name = _t('index.feed.title'); + self::$get_unread = self::$total_unread; + break; + case 's': + self::$current_get['starred'] = true; + self::$name = _t('index.feed.title_fav'); + self::$get_unread = self::$total_starred['unread']; + + // Update state if favorite is not yet enabled. + self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE; + break; + case 'f': + // We try to find the corresponding feed. + $feed = FreshRSS_CategoryDAO::findFeed(self::$categories, $id); + if ($feed === null) { + $feedDAO = FreshRSS_Factory::createFeedDao(); + $feed = $feedDAO->searchById($id); + + if (!$feed) { + throw new FreshRSS_Context_Exception('Invalid feed: ' . $id); + } + } + + self::$current_get['feed'] = $id; + self::$current_get['category'] = $feed->category(); + self::$name = $feed->name(); + self::$get_unread = $feed->nbNotRead(); + break; + case 'c': + // We try to find the corresponding category. + self::$current_get['category'] = $id; + if (!isset(self::$categories[$id])) { + $catDAO = new FreshRSS_CategoryDAO(); + $cat = $catDAO->searchById($id); + + if (!$cat) { + throw new FreshRSS_Context_Exception('Invalid category: ' . $id); + } + } else { + $cat = self::$categories[$id]; + } + + self::$name = $cat->name(); + self::$get_unread = $cat->nbNotRead(); + break; + default: + throw new FreshRSS_Context_Exception('Invalid getter: ' . $get); + } + + self::_nextGet(); + } + + /** + * Set the value of $next_get attribute. + */ + public static function _nextGet() { + $get = self::currentGet(); + // By default, $next_get == $get + self::$next_get = $get; + + if (self::$user_conf->onread_jump_next && strlen($get) > 2) { + $another_unread_id = ''; + $found_current_get = false; + switch ($get[0]) { + case 'f': + // We search the next feed with at least one unread article in + // same category as the currend feed. + foreach (self::$categories as $cat) { + if ($cat->id() != self::$current_get['category']) { + // We look into the category of the current feed! + continue; + } + + foreach ($cat->feeds() as $feed) { + if ($feed->id() == self::$current_get['feed']) { + // Here is our current feed! Fine, the next one will + // be a potential candidate. + $found_current_get = true; + continue; + } + + if ($feed->nbNotRead() > 0) { + $another_unread_id = $feed->id(); + if ($found_current_get) { + // We have found our current feed and now we + // have an feed with unread articles. Leave the + // loop! + break; + } + } + } + break; + } + + // If no feed have been found, next_get is the current category. + self::$next_get = empty($another_unread_id) ? + 'c_' . self::$current_get['category'] : + 'f_' . $another_unread_id; + break; + case 'c': + // We search the next category with at least one unread article. + foreach (self::$categories as $cat) { + if ($cat->id() == self::$current_get['category']) { + // Here is our current category! Next one could be our + // champion if it has unread articles. + $found_current_get = true; + continue; + } + + if ($cat->nbNotRead() > 0) { + $another_unread_id = $cat->id(); + if ($found_current_get) { + // Unread articles and the current category has + // already been found? Leave the loop! + break; + } + } + } + + // No unread category? The main stream will be our destination! + self::$next_get = empty($another_unread_id) ? + 'a' : + 'c_' . $another_unread_id; + break; + } + } + } + + /** + * Determine if the auto remove is available in the current context. + * This feature is available if: + * - it is activated in the configuration + * - the "read" state is not enable + * - the "unread" state is enable + * + * @return boolean + */ + public static function isAutoRemoveAvailable() { + if (!self::$user_conf->auto_remove_article) { + return false; + } + if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) { + return false; + } + if (!self::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ)) { + return false; + } + return true; + } + + /** + * Determine if the "sticky post" option is enabled. It can be enable + * by the user when it is selected in the configuration page or by the + * application when the context allows to auto-remove articles when they + * are read. + * + * @return boolean + */ + public static function isStickyPostEnabled() { + if (self::$user_conf->sticky_post) { + return true; + } + if (self::isAutoRemoveAvailable()) { + return true; + } + return false; + } +} diff --git a/sources/app/Models/DatabaseDAO.php b/sources/app/Models/DatabaseDAO.php new file mode 100755 index 0000000..0d85718 --- /dev/null +++ b/sources/app/Models/DatabaseDAO.php @@ -0,0 +1,83 @@ +bd->prepare($sql); + $stm->execute(); + $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 = 'DESC ' . $this->prefix . $table; + $stm = $this->bd->prepare($sql); + $stm->execute(); + + return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC)); + } + + public function checkTable($table, $schema) { + $columns = $this->getSchema($table); + + $ok = (count($columns) == count($schema)); + foreach ($columns as $c) { + $ok &= in_array($c['name'], $schema); + } + + return $ok; + } + + public function categoryIsCorrect() { + return $this->checkTable('category', array( + 'id', 'name' + )); + } + + public function feedIsCorrect() { + return $this->checkTable('feed', array( + 'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate', + 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', + 'cache_nbEntries', 'cache_nbUnreads' + )); + } + + public function entryIsCorrect() { + return $this->checkTable('entry', array( + 'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'is_read', + 'is_favorite', 'id_feed', 'tags' + )); + } + + public function daoToSchema($dao) { + return array( + 'name' => $dao['Field'], + 'type' => strtolower($dao['Type']), + 'notnull' => (bool)$dao['Null'], + 'default' => $dao['Default'], + ); + } + + public function listDaoToSchema($listDAO) { + $list = array(); + + foreach ($listDAO as $dao) { + $list[] = $this->daoToSchema($dao); + } + + return $list; + } +} diff --git a/sources/app/Models/DatabaseDAOSQLite.php b/sources/app/Models/DatabaseDAOSQLite.php new file mode 100755 index 0000000..7f53f96 --- /dev/null +++ b/sources/app/Models/DatabaseDAOSQLite.php @@ -0,0 +1,48 @@ +bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + + $tables = array( + 'category' => false, + 'feed' => false, + 'entry' => false, + ); + foreach ($res as $value) { + $tables[$value['name']] = true; + } + + return count(array_keys($tables, true, true)) == count($tables); + } + + public function getSchema($table) { + $sql = 'PRAGMA table_info(' . $table . ')'; + $stm = $this->bd->prepare($sql); + $stm->execute(); + + return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC)); + } + + public function entryIsCorrect() { + return $this->checkTable('entry', array( + 'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'is_read', + 'is_favorite', 'id_feed', 'tags' + )); + } + + public function daoToSchema($dao) { + return array( + 'name' => $dao['name'], + 'type' => strtolower($dao['type']), + 'notnull' => $dao['notnull'] === '1' ? true : false, + 'default' => $dao['dflt_value'], + ); + } +} diff --git a/sources/app/Models/Entry.php b/sources/app/Models/Entry.php index 9d7dd5d..346c98a 100755 --- a/sources/app/Models/Entry.php +++ b/sources/app/Models/Entry.php @@ -1,12 +1,11 @@ _guid ($guid); - $this->_title ($title); - $this->_author ($author); - $this->_content ($content); - $this->_link ($link); - $this->_date ($pubdate); - $this->_isRead ($is_read); - $this->_isFavorite ($is_favorite); - $this->_feed ($feed); - $this->_tags (preg_split('/[\s#]/', $tags)); + public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '', + $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') { + $this->_guid($guid); + $this->_title($title); + $this->_author($author); + $this->_content($content); + $this->_link($link); + $this->_date($pubdate); + $this->_isRead($is_read); + $this->_isFavorite($is_favorite); + $this->_feed($feed); + $this->_tags(preg_split('/[\s#]/', $tags)); } - public function id () { + public function id() { return $this->id; } - public function guid () { + public function guid() { return $this->guid; } - public function title () { + public function title() { return $this->title; } - public function author () { + public function author() { return $this->author === null ? '' : $this->author; } - public function content () { + public function content() { return $this->content; } - public function link () { + public function link() { return $this->link; } - public function date ($raw = false) { + public function date($raw = false) { if ($raw) { return $this->date; } else { - return timestamptodate ($this->date); + return timestamptodate($this->date); } } - public function dateAdded ($raw = false) { + public function dateAdded($raw = false) { $date = intval(substr($this->id, 0, -6)); if ($raw) { return $date; } else { - return timestamptodate ($date); + return timestamptodate($date); } } - public function isRead () { + public function isRead() { return $this->is_read; } - public function isFavorite () { + public function isFavorite() { return $this->is_favorite; } - public function feed ($object = false) { + public function feed($object = false) { if ($object) { $feedDAO = FreshRSS_Factory::createFeedDao(); - return $feedDAO->searchById ($this->feed); + return $feedDAO->searchById($this->feed); } else { return $this->feed; } } - public function tags ($inString = false) { + public function tags($inString = false) { if ($inString) { - return empty ($this->tags) ? '' : '#' . implode(' #', $this->tags); + return empty($this->tags) ? '' : '#' . implode(' #', $this->tags); } else { return $this->tags; } } - public function _id ($value) { + public function _id($value) { $this->id = $value; } - public function _guid ($value) { + public function _guid($value) { $this->guid = $value; } - public function _title ($value) { + public function _title($value) { $this->title = $value; } - public function _author ($value) { + public function _author($value) { $this->author = $value; } - public function _content ($value) { + public function _content($value) { $this->content = $value; } - public function _link ($value) { + public function _link($value) { $this->link = $value; } - public function _date ($value) { + public function _date($value) { $value = intval($value); $this->date = $value > 1 ? $value : time(); } - public function _isRead ($value) { + public function _isRead($value) { $this->is_read = $value; } - public function _isFavorite ($value) { + public function _isFavorite($value) { $this->is_favorite = $value; } - public function _feed ($value) { + public function _feed($value) { $this->feed = $value; } - public function _tags ($value) { - if (!is_array ($value)) { - $value = array ($value); + public function _tags($value) { + if (!is_array($value)) { + $value = array($value); } foreach ($value as $key => $t) { if (!$t) { - unset ($value[$key]); + unset($value[$key]); } } $this->tags = $value; } - public function isDay ($day, $today) { + public function isDay($day, $today) { $date = $this->dateAdded(true); switch ($day) { - case FreshRSS_Days::TODAY: - $tomorrow = $today + 86400; - return $date >= $today && $date < $tomorrow; - case FreshRSS_Days::YESTERDAY: - $yesterday = $today - 86400; - return $date >= $yesterday && $date < $today; - case FreshRSS_Days::BEFORE_YESTERDAY: - $yesterday = $today - 86400; - return $date < $yesterday; - default: - return false; + case FreshRSS_Days::TODAY: + $tomorrow = $today + 86400; + return $date >= $today && $date < $tomorrow; + case FreshRSS_Days::YESTERDAY: + $yesterday = $today - 86400; + return $date >= $yesterday && $date < $today; + case FreshRSS_Days::BEFORE_YESTERDAY: + $yesterday = $today - 86400; + return $date < $yesterday; + default: + return false; } } @@ -158,7 +157,7 @@ class FreshRSS_Entry extends Minz_Model { $entryDAO = FreshRSS_Factory::createEntryDao(); $entry = $entryDAO->searchByGuid($this->feed, $this->guid); - if($entry) { + if ($entry) { // l'article existe déjà en BDD, en se contente de recharger ce contenu $this->content = $entry->content(); } else { @@ -168,25 +167,25 @@ class FreshRSS_Entry extends Minz_Model { htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries ); } catch (Exception $e) { - // rien à faire, on garde l'ancien contenu (requête a échoué) + // rien à faire, on garde l'ancien contenu(requête a échoué) } } } } - public function toArray () { - return array ( - 'id' => $this->id (), - 'guid' => $this->guid (), - 'title' => $this->title (), - 'author' => $this->author (), - 'content' => $this->content (), - 'link' => $this->link (), - 'date' => $this->date (true), - 'is_read' => $this->isRead (), - 'is_favorite' => $this->isFavorite (), - 'id_feed' => $this->feed (), - 'tags' => $this->tags (true), + public function toArray() { + return array( + 'id' => $this->id(), + 'guid' => $this->guid(), + 'title' => $this->title(), + 'author' => $this->author(), + 'content' => $this->content(), + 'link' => $this->link(), + 'date' => $this->date(true), + 'is_read' => $this->isRead(), + 'is_favorite' => $this->isFavorite(), + 'id_feed' => $this->feed(), + 'tags' => $this->tags(true), ); } } diff --git a/sources/app/Models/EntryDAO.php b/sources/app/Models/EntryDAO.php index c1f87ee..61beeea 100755 --- a/sources/app/Models/EntryDAO.php +++ b/sources/app/Models/EntryDAO.php @@ -40,11 +40,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); if ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries - Minz_Log::record('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] - . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::ERROR); + Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] + . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']); } /*else { - Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] - . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::DEBUG); + Minz_Log::debug('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] + . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']); }*/ return false; } @@ -80,6 +80,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return -1; } + /** + * Toggle favorite marker on one or more article + * + * @todo simplify the query by removing the str_repeat. I am pretty sure + * there is an other way to do that. + * + * @param integer|array $ids + * @param boolean $is_favorite + * @return false|integer + */ public function markFavorite($ids, $is_favorite = true) { if (!is_array($ids)) { $ids = array($ids); @@ -94,11 +104,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markFavorite: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markFavorite: ' . $info[2]); return false; } } + /** + * Update the unread article cache held on every feed details. + * Depending on the parameters, it updates the cache on one feed, on all + * feeds from one category or on all feeds. + * + * @todo It can use the query builder refactoring to build that query + * + * @param false|integer $catId category ID + * @param false|integer $feedId feed ID + * @return boolean + */ protected function updateCacheUnreads($catId = false, $feedId = false) { $sql = 'UPDATE `' . $this->prefix . 'feed` f ' . 'LEFT OUTER JOIN (' @@ -124,11 +145,24 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return true; } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]); return false; } } + /** + * Toggle the read marker on one or more article. + * Then the cache is updated. + * + * @todo change the way the query is build because it seems there is + * unnecessary code in here. For instance, the part with the str_repeat. + * @todo remove code duplication. It seems the code is basically the + * same if it is an array or not. + * + * @param integer|array $ids + * @param boolean $is_read + * @return integer affected rows + */ public function markRead($ids, $is_read = true) { if (is_array($ids)) { //Many IDs at once (used by API) if (count($ids) < 6) { //Speed heuristics @@ -147,7 +181,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markRead: ' . $info[2]); return false; } $affected = $stm->rowCount(); @@ -166,16 +200,37 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markRead: ' . $info[2]); return false; } } } + /** + * Mark all entries as read depending on parameters. + * If $onlyFavorites is true, it is used when the user mark as read in + * the favorite pseudo-category. + * If $priorityMin is greater than 0, it is used when the user mark as + * read in the main feed pseudo-category. + * Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @todo refactor this method along with markReadCat and markReadFeed + * since they are all doing the same thing. I think we need to build a + * tool to generate the query instead of having queries all over the + * place. It will be reused also for the filtering making every thing + * separated. + * + * @param integer $idMax fail safe article ID + * @param boolean $onlyFavorites + * @param integer $priorityMin + * @return integer affected rows + */ public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { if ($idMax == 0) { $idMax = time() . '000000'; - Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG); + Minz_Log::debug('Calling markReadEntries(0) is deprecated!'); } $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id ' @@ -190,7 +245,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markReadEntries: ' . $info[2]); return false; } $affected = $stm->rowCount(); @@ -200,10 +255,21 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } + /** + * Mark all the articles in a category as read. + * There is a fail safe to prevent to mark as read articles that are + * loaded during the mark as read action. Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @param integer $id category ID + * @param integer $idMax fail safe article ID + * @return integer affected rows + */ public function markReadCat($id, $idMax = 0) { if ($idMax == 0) { $idMax = time() . '000000'; - Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG); + Minz_Log::debug('Calling markReadCat(0) is deprecated!'); } $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id ' @@ -213,7 +279,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markReadCat: ' . $info[2]); return false; } $affected = $stm->rowCount(); @@ -223,10 +289,21 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } + /** + * Mark all the articles in a feed as read. + * There is a fail safe to prevent to mark as read articles that are + * loaded during the mark as read action. Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @param integer $id feed ID + * @param integer $idMax fail safe article ID + * @return integer affected rows + */ public function markReadFeed($id, $idMax = 0) { if ($idMax == 0) { $idMax = time() . '000000'; - Minz_Log::record('Calling markReadFeed(0) is deprecated!', Minz_Log::DEBUG); + Minz_Log::debug('Calling markReadFeed(0) is deprecated!'); } $this->bd->beginTransaction(); @@ -237,7 +314,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markReadFeed: ' . $info[2]); $this->bd->rollBack(); return false; } @@ -251,7 +328,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markReadFeed: ' . $info[2]); $this->bd->rollBack(); return false; } @@ -299,7 +376,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL } - private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { + private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { if (!$state) { $state = FreshRSS_Entry::STATE_ALL; } @@ -307,34 +384,32 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $joinFeed = false; $values = array(); switch ($type) { - case 'a': - $where .= 'f.priority > 0 '; - $joinFeed = true; - break; - case 's': //Deprecated: use $state instead - $where .= 'e1.is_favorite=1 '; - break; - case 'c': - $where .= 'f.category=? '; - $values[] = intval($id); - $joinFeed = true; - break; - case 'f': - $where .= 'e1.id_feed=? '; - $values[] = intval($id); - break; - case 'A': - $where .= '1 '; - break; - default: - throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!'); + case 'a': + $where .= 'f.priority > 0 '; + $joinFeed = true; + break; + case 's': //Deprecated: use $state instead + $where .= 'e1.is_favorite=1 '; + break; + case 'c': + $where .= 'f.category=? '; + $values[] = intval($id); + $joinFeed = true; + break; + case 'f': + $where .= 'e1.id_feed=? '; + $values[] = intval($id); + break; + case 'A': + $where .= '1 '; + break; + default: + throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!'); } if ($state & FreshRSS_Entry::STATE_NOT_READ) { if (!($state & FreshRSS_Entry::STATE_READ)) { $where .= 'AND e1.is_read=0 '; - } elseif ($state & FreshRSS_Entry::STATE_STRICT) { - $where .= 'AND e1.is_read=0 '; } } elseif ($state & FreshRSS_Entry::STATE_READ) { @@ -356,23 +431,14 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { default: throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!'); } - if ($firstId === '' && parent::$sharedDbType === 'mysql') { - $firstId = $order === 'DESC' ? '9000000000'. '000000' : '0'; //MySQL optimization. Tested on MySQL 5.5 with 150k articles - } + /*if ($firstId === '' && parent::$sharedDbType === 'mysql') { + $firstId = $order === 'DESC' ? '9000000000'. '000000' : '0'; //MySQL optimization. TODO: check if this is needed again, after the filtering for old articles has been removed in 0.9-dev + }*/ if ($firstId !== '') { $where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' '; } - if (($date_min > 0) && ($type !== 's')) { - $where .= 'AND (e1.id >= ' . $date_min . '000000'; - if ($showOlderUnreadsorFavorites) { //Lax date constraint - $where .= ' OR e1.is_read=0 OR e1.is_favorite=1 OR (f.keep_history <> 0'; - if (intval($keepHistoryDefault) === 0) { - $where .= ' AND f.keep_history <> -2'; //default - } - $where .= ')'; - } - $where .= ') '; - $joinFeed = true; + if ($date_min > 0) { + $where .= 'AND e1.id >= ' . $date_min . '000000 '; } $search = ''; if ($filter !== '') { @@ -434,8 +500,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { . ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ } - public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { - list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min); $sql = 'SELECT e.id, e.guid, e.title, e.author, ' . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') @@ -452,8 +518,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return self::daoToEntry($stm->fetchAll(PDO::FETCH_ASSOC)); } - public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { //For API - list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { //For API + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min); $stm = $this->bd->prepare($sql); $stm->execute($values); @@ -520,7 +586,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } public function size($all = false) { - $db = Minz_Configuration::dataBase(); + $db = FreshRSS_Context::$system_conf->db; $sql = 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema=?'; //MySQL $values = array($db['base']); if (!$all) { diff --git a/sources/app/Models/EntryDAOSQLite.php b/sources/app/Models/EntryDAOSQLite.php index 9dc395c..ffe0f03 100755 --- a/sources/app/Models/EntryDAOSQLite.php +++ b/sources/app/Models/EntryDAOSQLite.php @@ -26,11 +26,24 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { return true; } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]); return false; } } + /** + * Toggle the read marker on one or more article. + * Then the cache is updated. + * + * @todo change the way the query is build because it seems there is + * unnecessary code in here. For instance, the part with the str_repeat. + * @todo remove code duplication. It seems the code is basically the + * same if it is an array or not. + * + * @param integer|array $ids + * @param boolean $is_read + * @return integer affected rows + */ public function markRead($ids, $is_read = true) { if (is_array($ids)) { //Many IDs at once (used by API) if (true) { //Speed heuristics //TODO: Not implemented yet for SQLite (so always call IDs one by one) @@ -47,7 +60,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markRead 1: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markRead 1: ' . $info[2]); $this->bd->rollBack(); return false; } @@ -59,7 +72,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markRead 2: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markRead 2: ' . $info[2]); $this->bd->rollBack(); return false; } @@ -69,10 +82,31 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } } + /** + * Mark all entries as read depending on parameters. + * If $onlyFavorites is true, it is used when the user mark as read in + * the favorite pseudo-category. + * If $priorityMin is greater than 0, it is used when the user mark as + * read in the main feed pseudo-category. + * Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @todo refactor this method along with markReadCat and markReadFeed + * since they are all doing the same thing. I think we need to build a + * tool to generate the query instead of having queries all over the + * place. It will be reused also for the filtering making every thing + * separated. + * + * @param integer $idMax fail safe article ID + * @param boolean $onlyFavorites + * @param integer $priorityMin + * @return integer affected rows + */ public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { if ($idMax == 0) { $idMax = time() . '000000'; - Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG); + Minz_Log::debug('Calling markReadEntries(0) is deprecated!'); } $sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=1 WHERE is_read=0 AND id <= ?'; @@ -85,7 +119,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markReadEntries: ' . $info[2]); return false; } $affected = $stm->rowCount(); @@ -95,10 +129,21 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { return $affected; } + /** + * Mark all the articles in a category as read. + * There is a fail safe to prevent to mark as read articles that are + * loaded during the mark as read action. Then the cache is updated. + * + * If $idMax equals 0, a deprecated debug message is logged + * + * @param integer $id category ID + * @param integer $idMax fail safe article ID + * @return integer affected rows + */ public function markReadCat($id, $idMax = 0) { if ($idMax == 0) { $idMax = time() . '000000'; - Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG); + Minz_Log::debug('Calling markReadCat(0) is deprecated!'); } $sql = 'UPDATE `' . $this->prefix . 'entry` ' @@ -109,7 +154,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error markReadCat: ' . $info[2]); return false; } $affected = $stm->rowCount(); @@ -124,6 +169,6 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } public function size($all = false) { - return @filesize(DATA_PATH . '/' . Minz_Session::param('currentUser', '_') . '.sqlite'); + return @filesize(join_path(DATA_PATH, 'users', $this->current_user, 'db.sqlite')); } } diff --git a/sources/app/Models/Factory.php b/sources/app/Models/Factory.php index 08569b2..db09d15 100755 --- a/sources/app/Models/Factory.php +++ b/sources/app/Models/Factory.php @@ -2,30 +2,39 @@ class FreshRSS_Factory { - public static function createFeedDao() { - $db = Minz_Configuration::dataBase(); - if ($db['type'] === 'sqlite') { - return new FreshRSS_FeedDAOSQLite(); + 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(); + return new FreshRSS_FeedDAO($username); } } - public static function createEntryDao() { - $db = Minz_Configuration::dataBase(); - if ($db['type'] === 'sqlite') { - return new FreshRSS_EntryDAOSQLite(); + 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(); + return new FreshRSS_EntryDAO($username); } } - public static function createStatsDAO() { - $db = Minz_Configuration::dataBase(); - if ($db['type'] === 'sqlite') { - return new FreshRSS_StatsDAOSQLite(); + 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(); + 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); } } diff --git a/sources/app/Models/Feed.php b/sources/app/Models/Feed.php index 03baf3a..5ce03be 100755 --- a/sources/app/Models/Feed.php +++ b/sources/app/Models/Feed.php @@ -40,7 +40,8 @@ class FreshRSS_Feed extends Minz_Model { public function hash() { if ($this->hash === null) { - $this->hash = hash('crc32b', Minz_Configuration::salt() . $this->url); + $salt = FreshRSS_Context::$system_conf->salt; + $this->hash = hash('crc32b', $salt . $this->url); } return $this->hash; } @@ -210,6 +211,10 @@ class FreshRSS_Feed extends Minz_Model { $url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url); } $feed = customSimplePie(); + if (substr($url, -11) === '#force_feed') { + $feed->force_feed(true); + $url = substr($url, 0, -11); + } $feed->set_feed_url($url); if (!$loadDetails) { //Only activates auto-discovery when adding a new feed $feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE); @@ -217,7 +222,8 @@ class FreshRSS_Feed extends Minz_Model { $mtime = $feed->init(); if ((!$mtime) || $feed->error()) { - throw new FreshRSS_Feed_Exception($feed->error() . ' [' . $url . ']'); + $errorMessage = $feed->error(); + throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Feed error' : $errorMessage) . ' [' . $url . ']'); } if ($loadDetails) { @@ -225,7 +231,7 @@ class FreshRSS_Feed extends Minz_Model { $subscribe_url = $feed->subscribe_url(false); $title = strtr(html_only_entity_decode($feed->get_title()), array('<' => '<', '>' => '>', '"' => '"')); //HTML to HTML-PRE //ENT_COMPAT except & - $this->_name($title == '' ? $this->url : $title); + $this->_name($title == '' ? $url : $title); $this->_website(html_only_entity_decode($feed->get_link())); $this->_description(html_only_entity_decode($feed->get_description())); @@ -234,19 +240,16 @@ class FreshRSS_Feed extends Minz_Model { $subscribe_url = $feed->subscribe_url(true); } - if ($subscribe_url !== null && $subscribe_url !== $this->url) { - if ($this->httpAuth != '') { - // on enlève les id si authentification HTTP - $subscribe_url = preg_replace('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url); - } - $this->_url($subscribe_url); + $clean_url = url_remove_credentials($subscribe_url); + if ($subscribe_url !== null && $subscribe_url !== $url) { + $this->_url($clean_url); } if (($mtime === true) ||($mtime > $this->lastUpdate)) { - syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url); + Minz_Log::notice('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url); $this->loadEntries($feed); // et on charge les articles du flux } else { - syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url); + Minz_Log::notice('FreshRSS use cache for ' . $clean_url); $this->entries = array(); } diff --git a/sources/app/Models/FeedDAO.php b/sources/app/Models/FeedDAO.php index b89ae20..74597c7 100755 --- a/sources/app/Models/FeedDAO.php +++ b/sources/app/Models/FeedDAO.php @@ -19,7 +19,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $this->bd->lastInsertId(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error addFeed: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error addFeed: ' . $info[2]); return false; } } @@ -77,7 +77,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error updateFeed: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error updateFeed: ' . $info[2]); return false; } } @@ -107,7 +107,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error updateLastUpdate: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error updateLastUpdate: ' . $info[2]); return false; } } @@ -131,7 +131,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error changeCategory: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error changeCategory: ' . $info[2]); return false; } } @@ -146,7 +146,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error deleteFeed: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error deleteFeed: ' . $info[2]); return false; } } @@ -160,7 +160,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error deleteFeedByCategory: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error deleteFeedByCategory: ' . $info[2]); return false; } } @@ -191,7 +191,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { $res = $stm->fetchAll(PDO::FETCH_ASSOC); $feed = current(self::daoToFeed($res)); - if (isset($feed)) { + if (isset($feed) && $feed !== false) { return $feed; } else { return null; @@ -289,7 +289,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error updateCachedValues: ' . $info[2]); return false; } } @@ -301,7 +301,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { $this->bd->beginTransaction(); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error truncate: ' . $info[2]); $this->bd->rollBack(); return false; } @@ -313,7 +313,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error truncate: ' . $info[2]); $this->bd->rollBack(); return false; } @@ -338,7 +338,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error cleanOldEntries: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error cleanOldEntries: ' . $info[2]); return false; } } diff --git a/sources/app/Models/FeedDAOSQLite.php b/sources/app/Models/FeedDAOSQLite.php index 0d18723..7599fda 100755 --- a/sources/app/Models/FeedDAOSQLite.php +++ b/sources/app/Models/FeedDAOSQLite.php @@ -11,7 +11,7 @@ class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error updateCachedValues: ' . $info[2]); return false; } } diff --git a/sources/app/Models/Log.php b/sources/app/Models/Log.php index d279445..df2de72 100755 --- a/sources/app/Models/Log.php +++ b/sources/app/Models/Log.php @@ -5,22 +5,22 @@ class FreshRSS_Log extends Minz_Model { private $level; private $information; - public function date () { + public function date() { return $this->date; } - public function level () { + public function level() { return $this->level; } - public function info () { + public function info() { return $this->information; } - public function _date ($date) { + public function _date($date) { $this->date = $date; } - public function _level ($level) { + public function _level($level) { $this->level = $level; } - public function _info ($information) { + public function _info($information) { $this->information = $information; } } diff --git a/sources/app/Models/LogDAO.php b/sources/app/Models/LogDAO.php index d1e5152..4c56e31 100755 --- a/sources/app/Models/LogDAO.php +++ b/sources/app/Models/LogDAO.php @@ -2,15 +2,15 @@ class FreshRSS_LogDAO { public static function lines() { - $logs = array (); - $handle = @fopen(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', 'r'); + $logs = array(); + $handle = @fopen(join_path(DATA_PATH, 'users', Minz_Session::param('currentUser', '_'), 'log.txt'), 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { - if (preg_match ('/^\[([^\[]+)\] \[([^\[]+)\] --- (.*)$/', $line, $matches)) { + if (preg_match('/^\[([^\[]+)\] \[([^\[]+)\] --- (.*)$/', $line, $matches)) { $myLog = new FreshRSS_Log (); - $myLog->_date ($matches[1]); - $myLog->_level ($matches[2]); - $myLog->_info ($matches[3]); + $myLog->_date($matches[1]); + $myLog->_level($matches[2]); + $myLog->_info($matches[3]); $logs[] = $myLog; } } @@ -20,6 +20,6 @@ class FreshRSS_LogDAO { } public static function truncate() { - file_put_contents(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', ''); + file_put_contents(join_path(DATA_PATH, 'users', Minz_Session::param('currentUser', '_'), 'log.txt'), ''); } } diff --git a/sources/app/Models/Share.php b/sources/app/Models/Share.php index b146db7..db6feda 100755 --- a/sources/app/Models/Share.php +++ b/sources/app/Models/Share.php @@ -1,44 +1,240 @@ $share_options) { + $share_options['type'] = $share_type; + self::register($share_options); + } + } + + /** + * Return the list of sharing options. + * @return an array of FreshRSS_Share objects. + */ + public static function enum() { + return self::$list_sharing; + } + + /** + * Return FreshRSS_Share object related to the given type. + * @param $type the share type, null if $type is not registered. + */ + public static function get($type) { + if (!isset(self::$list_sharing[$type])) { + return null; + } + + return self::$list_sharing[$type]; + } + + /** + * + */ + private $type = ''; + private $name = ''; + private $url_transform = ''; + private $transform = array(); + private $form_type = 'simple'; + private $help_url = ''; + private $custom_name = null; + private $base_url = null; + private $title = null; + private $link = null; + + /** + * Create a FreshRSS_Share object. + * @param $type is a unique string defining the kind of share option. + * @param $url_transform defines the url format to use in order to share. + * @param $transform is an array of transformations to apply on link and title. + * @param $form_type defines which form we have to use to complete. "simple" + * is typically for a centralized service while "advanced" is for + * decentralized ones. + * @param $help_url is an optional url to give help on this option. + */ + private function __construct($type, $url_transform, $transform = array(), + $form_type, $help_url = '') { + $this->type = $type; + $this->name = _t('gen.share.' . $type); + $this->url_transform = $url_transform; + $this->help_url = $help_url; + + if (!is_array($transform)) { + $transform = array(); + } + $this->transform = $transform; + + if (!in_array($form_type, array('simple', 'advanced'))) { + $form_type = 'simple'; + } + $this->form_type = $form_type; + } + + /** + * Update a FreshRSS_Share object with information from an array. + * @param $options is a list of informations to update where keys should be + * in this list: name, url, title, link. + */ + public function update($options) { + $available_options = array( + 'name' => 'custom_name', + 'url' => 'base_url', + 'title' => 'title', + 'link' => 'link', + ); + + foreach ($options as $key => $value) { + if (!isset($available_options[$key])) { + continue; + } + + $this->$available_options[$key] = $value; + } + } + + /** + * Return the current type of the share option. + */ + public function type() { + return $this->type; + } + + /** + * Return the current form type of the share option. + */ + public function formType() { + return $this->form_type; + } + + /** + * Return the current help url of the share option. + */ + public function help() { + return $this->help_url; + } + + /** + * Return the current name of the share option. + */ + public function name($real = false) { + if ($real || is_null($this->custom_name)) { + return $this->name; + } else { + return $this->custom_name; + } + } + + /** + * Return the current base url of the share option. + */ + public function baseUrl() { + return $this->base_url; + } + + /** + * Return the current url by merging url_transform and base_url. + */ + public function url() { $matches = array( '~URL~', '~TITLE~', '~LINK~', ); $replaces = array( - $selected['url'], - self::transformData($title, self::getTransform($share, 'title')), - self::transformData($link, self::getTransform($share, 'link')), + $this->base_url, + $this->title(), + $this->link(), ); - $url = str_replace($matches, $replaces, $share['url']); - return $url; + return str_replace($matches, $replaces, $this->url_transform); } - static private function transformData($data, $transform) { - if (!is_array($transform)) { - return $data; - } - if (count($transform) === 0) { + /** + * Return the title. + * @param $raw true if we should get the title without transformations. + */ + public function title($raw = false) { + if ($raw) { + return $this->title; + } + + return $this->transform($this->title, $this->getTransform('title')); + } + + /** + * Return the link. + * @param $raw true if we should get the link without transformations. + */ + public function link($raw = false) { + if ($raw) { + return $this->link; + } + + return $this->transform($this->link, $this->getTransform('link')); + } + + /** + * Transform a data with the given functions. + * @param $data the data to transform. + * @param $tranform an array containing a list of functions to apply. + * @return the transformed data. + */ + private static function transform($data, $transform) { + if (!is_array($transform) || empty($transform)) { return $data; } + foreach ($transform as $action) { $data = call_user_func($action, $data); } + return $data; } - static private function getTransform($options, $type) { - $transform = $options['transform']; - - if (array_key_exists($type, $transform)) { - return $transform[$type]; + /** + * Get the list of transformations for the given attribute. + * @param $attr the attribute of which we want the transformations. + * @return an array containing a list of transformations to apply. + */ + private function getTransform($attr) { + if (array_key_exists($attr, $this->transform)) { + return $this->transform[$attr]; } - return $transform; + return $this->transform; } - } diff --git a/sources/app/Models/StatsDAO.php b/sources/app/Models/StatsDAO.php index 08dd4cd..80caccc 100755 --- a/sources/app/Models/StatsDAO.php +++ b/sources/app/Models/StatsDAO.php @@ -6,18 +6,36 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo { /** * Calculates entry repartition for all feeds and for main stream. + * + * @return array + */ + public function calculateEntryRepartition() { + return array( + 'main_stream' => $this->calculateEntryRepartitionPerFeed(null, true), + 'all_feeds' => $this->calculateEntryRepartitionPerFeed(null, false), + ); + } + + /** + * Calculates entry repartition for the selection. * The repartition includes: * - total entries * - read entries * - unread entries * - favorite entries * - * @return type + * @param null|integer $feed feed id + * @param boolean $only_main + * @return array */ - public function calculateEntryRepartition() { - $repartition = array(); - - // Generates the repartition for the main stream of entry + public function calculateEntryRepartitionPerFeed($feed = null, $only_main = false) { + $filter = ''; + if ($only_main) { + $filter .= 'AND f.priority = 10'; + } + if (!is_null($feed)) { + $filter .= "AND e.id_feed = {$feed}"; + } $sql = <<prefix}entry AS e , {$this->prefix}feed AS f WHERE e.id_feed = f.id -AND f.priority = 10 +{$filter} SQL; $stm = $this->bd->prepare($sql); $stm->execute(); $res = $stm->fetchAll(PDO::FETCH_ASSOC); - $repartition['main_stream'] = $res[0]; - // Generates the repartition for all entries - $sql = <<prefix}entry AS e -SQL; - $stm = $this->bd->prepare($sql); - $stm->execute(); - $res = $stm->fetchAll(PDO::FETCH_ASSOC); - $repartition['all_feeds'] = $res[0]; - - return $repartition; + return $res[0]; } /** @@ -147,10 +151,9 @@ SQL; * @return string */ protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) { + $restrict = ''; if ($feed) { $restrict = "WHERE e.id_feed = {$feed}"; - } else { - $restrict = ''; } $sql = <<calculateEntryAveragePerFeedPerPeriod(1/24, $feed); + return $this->calculateEntryAveragePerFeedPerPeriod(1 / 24, $feed); } /** @@ -210,10 +213,9 @@ SQL; * @return integer */ protected function calculateEntryAveragePerFeedPerPeriod($period, $feed = null) { + $restrict = ''; if ($feed) { $restrict = "WHERE e.id_feed = {$feed}"; - } else { - $restrict = ''; } $sql = << '★', 'help' => 'ⓘ', 'icon' => '⊚', + 'import' => '⤓', 'key' => '⚿', 'link' => '↗', 'login' => '🔒', diff --git a/sources/app/Models/UserDAO.php b/sources/app/Models/UserDAO.php index 9f64fb4..b55766a 100755 --- a/sources/app/Models/UserDAO.php +++ b/sources/app/Models/UserDAO.php @@ -2,14 +2,14 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { public function createUser($username) { - $db = Minz_Configuration::dataBase(); + $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 . '_', Minz_Translate::t('default_category')); + $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 @@ -17,7 +17,7 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { if (is_array($SQL_CREATE_TABLES)) { $ok = true; foreach ($SQL_CREATE_TABLES as $instruction) { - $sql = sprintf($instruction, '', Minz_Translate::t('default_category')); + $sql = sprintf($instruction, '', _t('gen.short.default_category')); $stm = $userPDO->bd->prepare($sql); $ok &= ($stm && $stm->execute()); } @@ -28,17 +28,17 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { return true; } else { $info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error : ' . $info[2]); return false; } } public function deleteUser($username) { - $db = Minz_Configuration::dataBase(); + $db = FreshRSS_Context::$system_conf->db; require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); if ($db['type'] === 'sqlite') { - return unlink(DATA_PATH . '/' . $username . '.sqlite'); + return unlink(join_path(DATA_PATH, 'users', $username, 'db.sqlite')); } else { $userPDO = new Minz_ModelPdo($username); @@ -48,9 +48,21 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { return true; } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + Minz_Log::error('SQL error : ' . $info[2]); return false; } } } + + public static function exist($username) { + return is_dir(join_path(DATA_PATH , 'users', $username)); + } + + public static function touch($username) { + return touch(join_path(DATA_PATH , 'users', $username, 'config.php')); + } + + public static function mtime($username) { + return @filemtime(join_path(DATA_PATH , 'users', $username, 'config.php')); + } } diff --git a/sources/app/SQL/install.sql.mysql.php b/sources/app/SQL/install.sql.mysql.php index 16cb3a3..cf01591 100755 --- a/sources/app/SQL/install.sql.mysql.php +++ b/sources/app/SQL/install.sql.mysql.php @@ -57,5 +57,3 @@ INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s"); '); define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory'); - -define('SQL_SHOW_TABLES', 'SHOW tables;'); diff --git a/sources/app/SQL/install.sql.sqlite.php b/sources/app/SQL/install.sql.sqlite.php index 7988ada..30bca28 100755 --- a/sources/app/SQL/install.sql.sqlite.php +++ b/sources/app/SQL/install.sql.sqlite.php @@ -55,5 +55,3 @@ $SQL_CREATE_TABLES = array( ); define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory'); - -define('SQL_SHOW_TABLES', 'SELECT name FROM sqlite_master WHERE type="table"'); diff --git a/sources/app/actualize_script.php b/sources/app/actualize_script.php index 4c306b8..fc4f9bf 100755 --- a/sources/app/actualize_script.php +++ b/sources/app/actualize_script.php @@ -7,48 +7,80 @@ ob_implicit_flush(false); ob_start(); echo 'Results: ', "\n"; //Buffered -Minz_Configuration::init(); +if (defined('STDOUT')) { + $begin_date = date_create('now'); + fwrite(STDOUT, 'Starting feed actualization at ' . $begin_date->format('c') . "\n"); //Unbuffered +} + +// Set the header params ($_GET) to call the FRSS application. +$_GET['c'] = 'feed'; +$_GET['a'] = 'actualize'; +$_GET['ajax'] = 1; +$_GET['force'] = true; +$_SERVER['HTTP_HOST'] = ''; + + +$log_file = join_path(USERS_PATH, '_', 'log.txt'); + + +$app = new FreshRSS(); + +$system_conf = Minz_Configuration::get('system'); +$system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!) + +// Create the list of users to actualize. +// Users are processed in a random order but always start with admin $users = listUsers(); -shuffle($users); //Process users in random order -array_unshift($users, Minz_Configuration::defaultUser()); //But always start with admin -$users = array_unique($users); +shuffle($users); +if ($system_conf->default_user !== ''){ + array_unshift($users, $system_conf->default_user); + $users = array_unique($users); +} -foreach ($users as $myUser) { - syslog(LOG_INFO, 'FreshRSS actualize ' . $myUser); - if (defined('STDOUT')) { - fwrite(STDOUT, 'Actualize ' . $myUser . "...\n"); //Unbuffered + +$limits = $system_conf->limits; +$min_last_activity = time() - $limits['max_inactivity']; +foreach ($users as $user) { + if (($user !== $system_conf->default_user) && + (FreshRSS_UserDAO::mtime($user) < $min_last_activity)) { + Minz_Log::notice('FreshRSS skip inactive user ' . $user, $log_file); + if (defined('STDOUT')) { + fwrite(STDOUT, 'FreshRSS skip inactive user ' . $user . "\n"); //Unbuffered + } + continue; } - echo $myUser, ' '; //Buffered + Minz_Log::notice('FreshRSS actualize ' . $user, $log_file); + if (defined('STDOUT')) { + fwrite(STDOUT, 'Actualize ' . $user . "...\n"); //Unbuffered + } + echo $user, ' '; //Buffered - $_GET['c'] = 'feed'; - $_GET['a'] = 'actualize'; - $_GET['ajax'] = 1; - $_GET['force'] = true; - $_SERVER['HTTP_HOST'] = ''; - $freshRSS = new FreshRSS(); + Minz_Session::_param('currentUser', $user); + new Minz_ModelPdo($user); //TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init + FreshRSS_Auth::giveAccess(); + $app->init(); + $app->run(); - Minz_Configuration::_authType('none'); - - Minz_Session::init('FreshRSS'); - Minz_Session::_param('currentUser', $myUser); - - $freshRSS->init(); - $freshRSS->run(); if (!invalidateHttpCache()) { - syslog(LOG_NOTICE, 'FreshRSS write access problem in ' . LOG_PATH . '/*.log!'); + Minz_Log::notice('FreshRSS write access problem in ' . join_path(USERS_PATH, $user, 'log.txt'), + $log_file); if (defined('STDERR')) { - fwrite(STDERR, 'Write access problem in ' . LOG_PATH . '/*.log!' . "\n"); + fwrite(STDERR, 'Write access problem in ' . join_path(USERS_PATH, $user, 'log.txt') . "\n"); } } - Minz_Session::unset_session(true); - Minz_ModelPdo::clean(); } -syslog(LOG_INFO, 'FreshRSS actualize done.'); + + +Minz_Log::notice('FreshRSS actualize done.', $log_file); if (defined('STDOUT')) { fwrite(STDOUT, 'Done.' . "\n"); + $end_date = date_create('now'); + $duration = date_diff($end_date, $begin_date); + fwrite(STDOUT, 'Ending feed actualization at ' . $end_date->format('c') . "\n"); //Unbuffered + fwrite(STDOUT, 'Feed actualizations took ' . $duration->format('%a day(s), %h hour(s), %i minute(s) and %s seconds') . ' for ' . count($users) . " users\n"); //Unbuffered } echo 'End.', "\n"; ob_end_flush(); diff --git a/sources/app/i18n/de/admin.php b/sources/app/i18n/de/admin.php new file mode 100755 index 0000000..bcd0fcc --- /dev/null +++ b/sources/app/i18n/de/admin.php @@ -0,0 +1,170 @@ + array( + 'allow_anonymous' => 'Anonymes Lesen der Artikel des Standardbenutzers (%s) erlauben', + 'allow_anonymous_refresh' => 'Anonymes Aktualisieren der Artikel erlauben', + 'api_enabled' => 'API-Zugriff erlauben (für mobile Anwendungen benötigt)', + 'form' => 'Webformular (traditionell, benötigt JavaScript)', + 'http' => 'HTTP (HTTPS für erfahrene Benutzer)', + 'none' => 'Keine (gefährlich)', + 'persona' => 'Mozilla Persona (modern, benötigt JavaScript)', + 'title' => 'Authentifizierung', + 'title_reset' => 'Zurücksetzen der Authentifizierung', + 'token' => 'Authentifizierungs-Token', + 'token_help' => 'Erlaubt den Zugriff auf die RSS-Ausgabe des Standardbenutzers ohne Authentifizierung.', + 'type' => 'Authentifizierungsmethode', + 'unsafe_autologin' => 'Erlaube unsicheres automatisches Anmelden mit folgendem Format: ', + ), + 'check_install' => array( + 'cache' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/cache. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/cache sind in Ordnung.', + ), + 'categories' => array( + 'nok' => 'Die Tabelle category ist schlecht konfiguriert.', + 'ok' => 'Die Tabelle category ist in Ordnung.', + ), + 'connection' => array( + 'nok' => 'Verbindung zur Datenbank kann nicht aufgebaut werden.', + 'ok' => 'Verbindung zur Datenbank ist in Ordnung.', + ), + 'ctype' => array( + 'nok' => 'Ihnen fehlt eine benötigte Bibliothek für die Überprüfung von Zeichentypen (php-ctype).', + 'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).', + ), + 'curl' => array( + 'nok' => 'Ihnen fehlt cURL (Paket php5-curl).', + 'ok' => 'Sie haben die cURL-Erweiterung.', + ), + 'data' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data sind in Ordnung.', + ), + 'database' => 'Datenbank-Installation', + 'dom' => array( + 'nok' => 'Ihnen fehlt eine benötigte Bibliothek um DOM zu durchstöbern (Paket php-xml).', + 'ok' => 'Sie haben die benötigte Bibliothek um DOM zu durchstöbern.', + ), + 'entries' => array( + 'nok' => 'Die Tabelle entry ist schlecht konfiguriert.', + 'ok' => 'Die Tabelle entry ist in Ordnung.', + ), + 'favicons' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/favicons. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/favicons sind in Ordnung.', + ), + 'feeds' => array( + 'nok' => 'Die Tabelle feed ist schlecht konfiguriert.', + 'ok' => 'Die Tabelle feed ist in Ordnung.', + ), + 'files' => 'Datei-Installation', + 'json' => array( + 'nok' => 'Ihnen fehlt JSON (Paket php5-json).', + 'ok' => 'Sie haben die JSON-Erweiterung.', + ), + 'minz' => array( + 'nok' => 'Ihnen fehlt das Minz-Framework.', + 'ok' => 'Sie haben das Minz-Framework.', + ), + 'pcre' => array( + 'nok' => 'Ihnen fehlt eine benötigte Bibliothek für reguläre Ausdrücke (php-pcre).', + '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).', + ), + 'persona' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/persona. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/persona sind in Ordnung.', + ), + 'php' => array( + '_' => 'PHP-Installation', + 'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.', + 'ok' => 'Ihre PHP-Version ist %s, welche kompatibel mit FreshRSS ist.', + ), + 'tables' => array( + 'nok' => 'Es fehlen eine oder mehrere Tabellen in der Datenbank.', + 'ok' => 'Tabellen existieren in der Datenbank.', + ), + 'title' => 'Installationsüberprüfung', + 'tokens' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/tokens. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/tokens sind in Ordnung.', + ), + 'users' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/users. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/users sind in Ordnung.', + ), + 'zip' => array( + 'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php5-zip).', + 'ok' => 'Sie haben die ZIP-Erweiterung.', + ), + ), + 'extensions' => array( + 'disabled' => 'Deaktiviert', + 'empty_list' => 'Es gibt keine installierte Erweiterung.', + 'enabled' => 'Aktiviert', + 'no_configure_view' => 'Diese Erweiterung kann nicht konfiguriert werden.', + 'system' => array( + '_' => 'System-Erweiterungen', + 'no_rights' => 'System-Erweiterung (Sie haben keine Berechtigung dafür)', + ), + 'title' => 'Erweiterungen', + 'user' => 'Benutzer-Erweiterungen', + ), + 'stats' => array( + '_' => 'Statistiken', + 'all_feeds' => 'Alle Feeds', + 'category' => 'Kategorie', + 'entry_count' => 'Anzahl der Einträge', + 'entry_per_category' => 'Einträge pro Kategorie', + 'entry_per_day' => 'Einträge pro Tag (letzte 30 Tage)', + 'entry_per_day_of_week' => 'Pro Wochentag (Durchschnitt: %.2f Nachrichten)', + 'entry_per_hour' => 'Pro Stunde (Durchschnitt: %.2f Nachrichten)', + 'entry_per_month' => 'Pro Monat (Durchschnitt: %.2f Nachrichten)', + 'entry_repartition' => 'Einträge-Verteilung', + 'feed' => 'Feed', + 'feed_per_category' => 'Feeds pro Kategorie', + 'idle' => 'Untätige Feeds', + 'main' => 'Haupt-Statistiken', + 'main_stream' => 'Haupt-Feeds', + 'menu' => array( + 'idle' => 'Untätige Feeds', + 'main' => 'Haupt-Statistiken', + 'repartition' => 'Artikel-Verteilung', + ), + 'no_idle' => 'Es gibt keinen untätigen Feed!', + 'number_entries' => '%d Artikel', + 'percent_of_total' => '%% Gesamt', + 'repartition' => 'Artikel-Verteilung', + 'status_favorites' => 'Favoriten', + 'status_read' => 'Gelesen', + 'status_total' => 'Gesamt', + 'status_unread' => 'Ungelesen', + 'title' => 'Statistiken', + 'top_feed' => 'Top 10-Feeds', + ), + 'update' => array( + '_' => 'System aktualisieren', + 'apply' => 'Anwenden', + 'check' => 'Auf neue Aktualisierungen prüfen', + 'current_version' => 'Ihre aktuelle Version von FreshRSS ist %s.', + 'last' => 'Letzte Überprüfung: %s', + 'none' => 'Keine Aktualisierung zum Anwenden', + 'title' => 'System aktualisieren', + ), + 'user' => array( + 'articles_and_size' => '%s Artikel (%s)', + 'create' => 'Neuen Benutzer erstellen', + 'email_persona' => 'Anmelde-E-Mail-Adresse
(für Mozilla Persona)', + 'language' => 'Sprache', + 'password_form' => 'Passwort
(für die Anmeldemethode per Webformular)', + 'password_format' => 'mindestens 7 Zeichen', + 'title' => 'Benutzer verwalten', + 'user_list' => 'Liste der Benutzer', + 'username' => 'Nutzername', + 'users' => 'Benutzer', + ), +); diff --git a/sources/app/i18n/de/conf.php b/sources/app/i18n/de/conf.php new file mode 100755 index 0000000..64c2c09 --- /dev/null +++ b/sources/app/i18n/de/conf.php @@ -0,0 +1,169 @@ + array( + '_' => 'Archivierung', + 'advanced' => 'Erweitert', + 'delete_after' => 'Entferne Artikel nach', + 'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Nachrichten-Feeds vorhanden.', + 'keep_history_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten wird', + 'optimize' => 'Datenbank optimieren', + 'optimize_help' => 'Sollte gelegentlich durchgeführt werden, um die Größe der Datenbank zu reduzieren.', + 'purge_now' => 'Jetzt bereinigen', + 'title' => 'Archivierung', + 'ttl' => 'Aktualisiere automatisch nicht öfter als', + ), + 'display' => array( + '_' => 'Anzeige', + 'icon' => array( + 'bottom_line' => 'Fußzeile', + 'entry' => 'Artikel-Symbole', + 'publication_date' => 'Datum der Veröffentlichung', + 'related_tags' => 'Verwandte Tags', + 'sharing' => 'Teilen', + 'top_line' => 'Kopfzeile', + ), + 'language' => 'Sprache', + 'notif_html5' => array( + 'seconds' => 'Sekunden (0 bedeutet keine Zeitüberschreitung)', + 'timeout' => 'Zeitüberschreitung für HTML5-Benachrichtigung', + ), + 'theme' => 'Erscheinungsbild', + 'title' => 'Anzeige', + 'width' => array( + 'content' => 'Inhaltsbreite', + 'large' => 'Weit', + 'medium' => 'Mittel', + 'no_limit' => 'Keine Begrenzung', + 'thin' => 'Schmal', + ), + ), + 'query' => array( + '_' => 'Benutzerabfragen', + 'deprecated' => 'Diese Abfrage ist nicht länger gültig. Die referenzierte Kategorie oder der Feed ist gelöscht worden.', + 'filter' => 'Angewendeter Filter:', + 'get_all' => 'Alle Artikel anzeigen', + 'get_category' => 'Kategorie "%s" anzeigen', + 'get_favorite' => 'Lieblingsartikel anzeigen', + 'get_feed' => 'Feed "%s" anzeigen', + 'no_filter' => 'Kein Filter', + 'none' => 'Sie haben bisher keine Benutzerabfrage erstellt.', + 'number' => 'Abfrage Nr. %d', + 'order_asc' => 'Älteste Artikel zuerst anzeigen', + 'order_desc' => 'Neueste Artikel zuerst anzeigen', + 'search' => 'Suche nach "%s"', + 'state_0' => 'Alle Artikel anzeigen', + 'state_1' => 'Gelesene Artikel anzeigen', + 'state_2' => 'Ungelesene Artikel anzeigen', + 'state_3' => 'Alle Artikel anzeigen', + 'state_4' => 'Lieblingsartikel anzeigen', + 'state_5' => 'Gelesene Lieblingsartikel anzeigen', + 'state_6' => 'Ungelesene Lieblingsartikel anzeigen', + 'state_7' => 'Lieblingsartikel anzeigen', + 'state_8' => 'Keine Lieblingsartikel anzeigen', + 'state_9' => 'Gelesene ohne Lieblingsartikel anzeigen', + 'state_10' => 'Ungelesene ohne Lieblingsartikel anzeigen', + 'state_11' => 'Keine Lieblingsartikel anzeigen', + 'state_12' => 'Alle Artikel anzeigen', + 'state_13' => 'Gelesene Artikel anzeigen', + 'state_14' => 'Ungelesene Artikel anzeigen', + 'state_15' => 'Alle Artikel anzeigen', + 'title' => 'Benutzerabfragen', + ), + 'profile' => array( + '_' => 'Profil-Verwaltung', + 'email_persona' => 'Anmelde-E-Mail-Adresse
(für Mozilla Persona)', + 'password_api' => 'Passwort-API
(z. B. für mobile Anwendungen)', + 'password_form' => 'Passwort
(für die Anmeldemethode per Webformular)', + 'password_format' => 'mindestens 7 Zeichen', + 'title' => 'Profil', + ), + 'reading' => array( + '_' => 'Lesen', + 'after_onread' => 'Nach „Alle als gelesen markieren“,', + 'articles_per_page' => 'Anzahl der Artikel pro Seite', + 'auto_load_more' => 'Die nächsten Artikel am Seitenende laden', + 'auto_remove_article' => 'Artikel nach dem Lesen verstecken', + 'confirm_enabled' => 'Bei der Aktion „Alle als gelesen markieren“ einen Bestätigungsdialog anzeigen', + 'display_articles_unfolded' => 'Artikel standardmäßig ausgeklappt zeigen', + 'display_categories_unfolded' => 'Kategorien standardmäßig eingeklappt zeigen', + 'hide_read_feeds' => 'Kategorien & Feeds ohne ungelesene Artikel verstecken (funktioniert nicht mit der Einstellung „Alle Artikel zeigen“)', + 'img_with_lazyload' => 'Verwende die "träges Laden"-Methode zum Laden von Bildern', + 'jump_next' => 'springe zum nächsten ungelesenen Geschwisterelement (Feed oder Kategorie)', + 'number_divided_when_reader' => 'Geteilt durch 2 in der Lese-Ansicht.', + 'read' => array( + 'article_open_on_website' => 'wenn der Artikel auf der Original-Webseite geöffnet wird', + 'article_viewed' => 'wenn der Artikel angesehen wird', + 'scroll' => 'beim Blättern', + 'upon_reception' => 'beim Empfang des Artikels', + 'when' => 'Artikel als gelesen markieren…', + ), + 'show' => array( + '_' => 'Artikel zum Anzeigen', + 'adaptive' => 'Anzeige anpassen', + 'all_articles' => 'Alle Artikel zeigen', + 'unread' => 'Nur ungelesene zeigen', + ), + 'sort' => array( + '_' => 'Sortierreihenfolge', + 'newer_first' => 'Neuere zuerst', + 'older_first' => 'Ältere zuerst', + ), + 'sticky_post' => 'Wenn geöffnet, den Artikel ganz oben anheften', + 'title' => 'Lesen', + 'view' => array( + 'default' => 'Standard-Ansicht', + 'global' => 'Globale Ansicht', + 'normal' => 'Normale Ansicht', + 'reader' => 'Lese-Ansicht', + ), + ), + 'sharing' => array( + '_' => 'Teilen', + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'E-Mail', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'more_information' => 'Weitere Informationen', + 'print' => 'Drucken', + 'shaarli' => 'Shaarli', + 'share_name' => 'Anzuzeigender Teilen-Name', + 'share_url' => 'Zu verwendende Teilen-URL', + 'title' => 'Teilen', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'shortcut' => array( + '_' => 'Tastaturkürzel', + 'article_action' => 'Artikelaktionen', + 'auto_share' => 'Teilen', + 'auto_share_help' => 'Wenn es nur eine Option zum Teilen gibt, wird diese verwendet. Ansonsten sind die Optionen über ihre Nummer erreichbar.', + 'close_dropdown' => 'Menüs schließen', + 'collapse_article' => 'Zusammenfalten', + 'first_article' => 'Zum ersten Artikel springen', + 'focus_search' => 'Auf Suchfeld zugreifen', + 'help' => 'Dokumentation anzeigen', + 'javascript' => 'JavaScript muss aktiviert sein, um Tastaturkürzel benutzen zu können', + 'last_article' => 'Zum letzten Artikel springen', + 'load_more' => 'Weitere Artikel laden', + 'mark_read' => 'Als gelesen markieren', + 'mark_favorite' => 'Als Favorit markieren', + 'navigation' => 'Navigation', + 'navigation_help' => 'Mit der "Umschalttaste" finden die Tastaturkürzel auf Feeds Anwendung.
Mit der "Alt-Taste" finden die Tastaturkürzel auf Kategorien Anwendung.', + 'next_article' => 'Zum nächsten Artikel springen', + 'other_action' => 'Andere Aktionen', + 'previous_article' => 'Zum vorherigen Artikel springen', + 'see_on_website' => 'Auf der Original-Webseite ansehen', + 'shift_for_all_read' => '+ Umschalttaste, um alle Artikel als gelesen zu markieren.', + 'title' => 'Tastaturkürzel', + 'user_filter' => 'Auf Benutzerfilter zugreifen', + 'user_filter_help' => 'Wenn es nur einen Benutzerfilter gibt, wird dieser verwendet. Ansonsten sind die Filter über ihre Nummer erreichbar.', + ), + 'user' => array( + 'articles_and_size' => '%s Artikel (%s)', + 'current' => 'Aktueller Benutzer', + 'is_admin' => 'ist Administrator', + 'users' => 'Benutzer', + ), +); diff --git a/sources/app/i18n/de/feedback.php b/sources/app/i18n/de/feedback.php new file mode 100755 index 0000000..48f8b74 --- /dev/null +++ b/sources/app/i18n/de/feedback.php @@ -0,0 +1,110 @@ + array( + 'optimization_complete' => 'Optimierung abgeschlossen', + ), + 'access' => array( + 'denied' => 'Sie haben nicht die Berechtigung, diese Seite aufzurufen', + 'not_found' => 'Sie suchen nach einer Seite, die nicht existiert', + ), + 'auth' => array( + 'form' => array( + 'not_set' => 'Während der Konfiguration des Authentifikationssystems trat ein Fehler auf. Bitte versuchen Sie es später erneut.', + 'set' => 'Formular ist ab sofort ihr Standard-Authentifikationssystem.', + ), + 'login' => array( + 'invalid' => 'Anmeldung ist ungültig', + 'success' => 'Sie sind verbunden', + ), + 'logout' => array( + 'success' => 'Sie sind getrennt', + ), + 'no_password_set' => 'Administrator-Passwort ist nicht gesetzt worden. Dieses Feature ist nicht verfügbar.', + 'not_persona' => 'Nur das Persona-System kann zurückgesetzt werden.', + ), + 'conf' => array( + 'error' => 'Während des Speicherung der Konfiguration trat ein Fehler auf', + 'query_created' => 'Abfrage "%s" ist erstellt worden.', + 'shortcuts_updated' => 'Tastaturkürzel sind aktualisiert worden', + 'updated' => 'Konfiguration ist aktualisiert worden', + ), + 'extensions' => array( + 'already_enabled' => '%s ist bereits aktiviert', + 'disable' => array( + 'ko' => '%s kann nicht deaktiviert werden. Für Details prüfen Sie die FressRSS-Protokolle.', + 'ok' => '%s ist jetzt deaktiviert', + ), + 'enable' => array( + 'ko' => '%s kann nicht aktiviert werden. Für Details prüfen Sie die FressRSS-Protokolle.', + 'ok' => '%s ist jetzt aktiviert', + ), + 'no_access' => 'Sie haben keinen Zugang zu %s', + 'not_enabled' => '%s ist noch nicht aktiviert', + 'not_found' => '%s existiert nicht', + ), + 'import_export' => array( + 'export_no_zip_extension' => 'Die Zip-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie, Dateien eine nach der anderen zu exportieren.', + 'feeds_imported' => 'Ihre Feeds sind importiert worden und werden jetzt aktualisiert', + 'feeds_imported_with_errors' => 'Ihre Feeds sind importiert worden, aber es traten einige Fehler auf', + 'file_cannot_be_uploaded' => 'Datei kann nicht hochgeladen werden!', + 'no_zip_extension' => 'Die Zip-Erweiterung ist auf Ihrem Server nicht vorhanden.', + 'zip_error' => 'Ein Fehler trat während des Zip-Imports auf.', + ), + 'sub' => array( + 'actualize' => 'Aktualisieren', + 'category' => array( + 'created' => 'Kategorie %s ist erstellt worden.', + 'deleted' => 'Kategorie ist gelöscht worden.', + 'emptied' => 'Kategorie ist geleert worden.', + 'error' => 'Kategorie kann nicht aktualisiert werden', + 'name_exists' => 'Kategorie-Name existiert bereits.', + 'no_id' => 'Sie müssen die ID der Kategorie präzisieren.', + 'no_name' => 'Kategorie-Name kann nicht leer sein.', + 'not_delete_default' => 'Sie können die Vorgabe-Kategorie nicht löschen!', + 'not_exist' => 'Die Kategorie existiert nicht!', + 'over_max' => 'Sie haben Ihr Kategorien-Limit erreicht (%d)', + 'updated' => 'Kategorie ist aktualisiert worden.', + ), + 'feed' => array( + 'actualized' => '%s ist aktualisiert worden', + 'actualizeds' => 'RSS-Feeds sind aktualisiert worden', + 'added' => 'RSS-Feed %s ist hinzugefügt worden', + 'already_subscribed' => 'Sie haben %s bereits abonniert', + 'deleted' => 'Feed ist gelöscht worden', + 'error' => 'Feed kann nicht aktualisiert werden', + 'internal_problem' => 'Der RSS-Feed konnte nicht hinzugefügt werden. Für Details prüfen Sie die FressRSS-Protokolle.', + 'invalid_url' => 'URL %s ist ungültig', + 'marked_read' => 'Feeds sind als gelesen markiert worden', + 'n_actualized' => '%d Feeds sind aktualisiert worden', + 'n_entries_deleted' => '%d Artikel sind gelöscht worden', + 'no_refresh' => 'Es gibt keinen Feed zum Aktualisieren…', + 'not_added' => '%s konnte nicht hinzugefügt werden', + 'over_max' => 'Sie haben Ihr Feeds-Limit erreicht (%d)', + 'updated' => 'Feed ist aktualisiert worden', + ), + 'purge_completed' => 'Bereinigung abgeschlossen (%d Artikel gelöscht)', + ), + 'update' => array( + 'can_apply' => 'FreshRSS wird nun auf die Version %s aktualisiert.', + 'error' => 'Der Aktualisierungsvorgang stieß auf einen Fehler: %s', + 'file_is_nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses %s. Der HTTP-Server muss Schreibrechte besitzen', + 'finished' => 'Aktualisierung abgeschlossen!', + 'none' => 'Keine Aktualisierung zum Anwenden', + 'server_not_found' => 'Aktualisierungs-Server kann nicht gefunden werden. [%s]', + ), + 'user' => array( + 'created' => array( + '_' => 'Benutzer %s ist erstellt worden', + 'error' => 'Benutzer %s kann nicht erstellt werden', + ), + 'deleted' => array( + '_' => 'Benutzer %s ist gelöscht worden', + 'error' => 'Benutzer %s kann nicht gelöscht werden', + ), + ), + 'profile' => array( + 'error' => 'Ihr Profil kann nicht geändert werden', + 'updated' => 'Ihr Profil ist geändert worden', + ), +); diff --git a/sources/app/i18n/de/gen.php b/sources/app/i18n/de/gen.php new file mode 100755 index 0000000..f3479ed --- /dev/null +++ b/sources/app/i18n/de/gen.php @@ -0,0 +1,163 @@ + array( + 'actualize' => 'Aktualisieren', + 'back_to_rss_feeds' => '← Zurück zu Ihren RSS-Feeds gehen', + 'cancel' => 'Abbrechen', + 'create' => 'Erstellen', + 'disable' => 'Deaktivieren', + 'empty' => 'Leeren', + 'enable' => 'Aktivieren', + 'export' => 'Exportieren', + 'filter' => 'Filtern', + 'import' => 'Importieren', + 'manage' => 'Verwalten', + 'mark_read' => 'Als gelesen markieren', + 'mark_favorite' => 'Als Favorit markieren', + 'remove' => 'Entfernen', + 'see_website' => 'Webseite ansehen', + 'submit' => 'Abschicken', + 'truncate' => 'Alle Artikel löschen', + ), + 'auth' => array( + 'keep_logged_in' => 'Eingeloggt bleiben (1 Monat)', + 'login' => 'Anmelden', + 'login_persona' => 'Anmelden mit Persona', + 'login_persona_problem' => 'Verbindungsproblem mit Persona?', + 'logout' => 'Abmelden', + 'password' => 'Passwort', + 'reset' => 'Zurücksetzen der Authentifizierung', + 'username' => 'Nutzername', + 'username_admin' => 'Administrator-Nutzername', + 'will_reset' => 'Authentifikationssystem wird zurückgesetzt: ein Formular wird anstelle von Persona benutzt.', + ), + 'date' => array( + 'Apr' => '\\A\\p\\r\\i\\l', + 'Aug' => '\\A\\u\\g\\u\\s\\t', + 'Dec' => '\\D\\e\\z\\e\\m\\b\\e\\r', + 'Feb' => '\\F\\e\\b\\r\\u\\a\\r', + 'Jan' => '\\J\\a\\n\\u\\a\\r', + 'Jul' => '\\J\\u\\l\\i', + 'Jun' => '\\J\\u\\n\\i', + 'Mar' => '\\M\\ä\\r\\z', + 'May' => '\\M\\a\\i', + 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r', + 'Oct' => '\\O\\k\\t\\o\\b\\e\\r', + 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r', + 'apr' => 'Apr', + 'april' => 'April', + 'aug' => 'Aug', + 'august' => 'August', + 'before_yesterday' => 'Vor gestern', + 'dec' => 'Dez', + 'december' => 'Dezember', + 'feb' => 'Feb', + 'february' => 'Februar', + 'format_date' => 'd\\. %s Y', + 'format_date_hour' => 'd\\. %s Y \\u\\m H\\:i', + 'fri' => 'Fr', + 'jan' => 'Jan', + 'january' => 'Januar', + 'jul' => 'Jul', + 'july' => 'Juli', + 'jun' => 'Jun', + 'june' => 'Juni', + 'last_3_month' => 'Letzte drei Monate', + 'last_6_month' => 'Letzte sechs Monate', + 'last_month' => 'Letzter Monat', + 'last_week' => 'Letzte Woche', + 'last_year' => 'Letztes Jahr', + 'mar' => 'Mär', + 'march' => 'März', + 'may' => 'Mai', + 'mon' => 'Mo', + 'month' => 'Monat(en)', + 'nov' => 'Nov', + 'november' => 'November', + 'oct' => 'Okt', + 'october' => 'Oktober', + 'sat' => 'Sa', + 'sep' => 'Sep', + 'september' => 'September', + 'sun' => 'So', + 'thu' => 'Do', + 'today' => 'Heute', + 'tue' => 'Di', + 'wed' => 'Mi', + 'yesterday' => 'Gestern', + ), + 'freshrss' => array( + '_' => 'FreshRSS', + 'about' => 'Über FreshRSS', + ), + 'js' => array( + 'category_empty' => 'Kategorie leeren', + 'confirm_action' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Dies kann nicht abgebrochen werden!', + 'confirm_action_feed_cat' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Sie werden zugehörige Favoriten und Benutzerabfragen verlieren. Dies kann nicht abgebrochen werden!', + 'feedback' => array( + 'body_new_articles' => 'Es gibt \\d neue Artikel zum Lesen auf FreshRSS.', + 'request_failed' => 'Eine Anfrage ist fehlgeschlagen, dies könnte durch Probleme mit der Internetverbindung verursacht worden sein.', + 'title_new_articles' => 'FreshRSS: neue Artikel!', + ), + 'new_article' => 'Es gibt neue verfügbare Artikel. Klicken Sie, um die Seite zu aktualisieren.', + 'should_be_activated' => 'JavaScript muss aktiviert sein', + ), + 'lang' => array( + 'de' => 'Deutsch', + 'en' => 'English', + 'fr' => 'Français', + ), + 'menu' => array( + 'about' => 'Über', + 'admin' => 'Administration', + 'archiving' => 'Archivierung', + 'authentication' => 'Authentifizierung', + 'check_install' => 'Installationsüberprüfung', + 'configuration' => 'Konfiguration', + 'display' => 'Anzeige', + 'extensions' => 'Erweiterungen', + 'logs' => 'Protokolle', + 'queries' => 'Benutzerabfragen', + 'reading' => 'Lesen', + 'search' => 'Suche Worte oder #Tags', + 'sharing' => 'Teilen', + 'shortcuts' => 'Tastaturkürzel', + 'stats' => 'Statistiken', + 'update' => 'Aktualisieren', + 'user_management' => 'Benutzer verwalten', + 'user_profile' => 'Profil', + ), + 'pagination' => array( + 'first' => 'Erste', + 'last' => 'Letzte', + 'load_more' => 'Weitere Artikel laden', + 'mark_all_read' => 'Alle als gelesen markieren', + 'next' => 'Nächste', + 'nothing_to_load' => 'Es gibt keine weiteren Artikel', + 'previous' => 'Vorherige', + ), + 'share' => array( + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'E-Mail', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'print' => 'Drucken', + 'shaarli' => 'Shaarli', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'short' => array( + 'attention' => 'Achtung!', + 'blank_to_disable' => 'Zum Deaktivieren frei lassen', + 'by_author' => 'Von %s', + 'by_default' => 'standardmäßig', + 'damn' => 'Verdammt!', + 'default_category' => 'Unkategorisiert', + 'no' => 'Nein', + 'ok' => 'OK!', + 'or' => 'oder', + 'yes' => 'Ja', + ), +); diff --git a/sources/app/i18n/de/index.php b/sources/app/i18n/de/index.php new file mode 100755 index 0000000..3449de8 --- /dev/null +++ b/sources/app/i18n/de/index.php @@ -0,0 +1,61 @@ + array( + '_' => 'Über', + 'agpl3' => 'AGPL 3', + 'bugs_reports' => 'Fehlerberichte', + 'credits' => 'Credits', + 'credits_content' => 'Einige Designelemente stammen von Bootstrap, obwohl FreshRSS dieses Framework nicht nutzt. Icons stammen vom GNOME project. Open Sans Font wurde von Steve Matteson erstellt. Favicons werden mit getFavicon API gesammelt. FreshRSS basiert auf Minz, einem PHP-Framework.', + 'freshrss_description' => 'FreshRSS ist ein RSS-Feedsaggregator zum selbst hosten wie zum Beispiel Kriss Feed oder Leed. Er ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstarkes und konfigurierbares Werkzeug.', + 'github' => 'on Github', + 'license' => 'Lizenz', + 'project_website' => 'Projekt-Webseite', + 'title' => 'Über', + 'version' => 'Version', + 'website' => 'Webseite', + ), + 'feed' => array( + 'add' => 'Sie können Feeds hinzufügen.', + 'empty' => 'Es gibt keinen Artikel zum Zeigen.', + 'rss_of' => 'RSS-Feed von %s', + 'title' => 'Ihre RSS-Feeds', + 'title_global' => 'Globale Ansicht', + 'title_fav' => 'Ihre Favoriten', + ), + 'log' => array( + '_' => 'Protokolle', + 'clear' => 'Protokolle leeren', + 'empty' => 'Protokolldatei ist leer.', + 'title' => 'Protokolle', + ), + 'menu' => array( + 'about' => 'Über FreshRSS', + 'add_query' => 'Eine Abfrage hinzufügen', + 'before_one_day' => 'Vor einem Tag', + 'before_one_week' => 'Vor einer Woche', + 'favorites' => 'Favoriten (%s)', + 'global_view' => 'Globale Ansicht', + 'main_stream' => 'Haupt-Feeds', + 'mark_all_read' => 'Alle als gelesen markieren', + 'mark_cat_read' => 'Kategorie als gelesen markieren', + 'mark_feed_read' => 'Feed als gelesen markieren', + 'newer_first' => 'Neuere zuerst', + 'non-starred' => 'Alle außer Favoriten zeigen', + 'normal_view' => 'Normale Ansicht', + 'older_first' => 'Ältere zuerst', + 'queries' => 'Benutzerabfragen', + 'read' => 'Nur gelesene zeigen', + 'reader_view' => 'Lese-Ansicht', + 'rss_view' => 'RSS-Feed', + 'search_short' => 'Suchen', + 'starred' => 'Nur Favoriten zeigen', + 'stats' => 'Statistiken', + 'subscription' => 'Abonnementverwaltung', + 'unread' => 'Nur ungelesene zeigen', + ), + 'share' => 'Teilen', + 'tag' => array( + 'related' => 'Verwandte Tags', + ), +); diff --git a/sources/app/i18n/de/install.php b/sources/app/i18n/de/install.php new file mode 100755 index 0000000..e9267bb --- /dev/null +++ b/sources/app/i18n/de/install.php @@ -0,0 +1,107 @@ + array( + 'finish' => 'Installation fertigstellen', + 'fix_errors_before' => 'Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.', + 'next_step' => 'Zum nächsten Schritt gehen', + ), + 'auth' => array( + 'email_persona' => 'Anmelde-E-Mail-Adresse
(für Mozilla Persona)', + 'form' => 'Webformular (traditionell, benötigt JavaScript)', + 'http' => 'HTTP (HTTPS für erfahrene Benutzer)', + 'none' => 'Keine (gefährlich)', + 'password_form' => 'Passwort
(für die Anmeldemethode per Webformular)', + 'password_format' => 'mindestens 7 Zeichen', + 'persona' => 'Mozilla Persona (modern, benötigt JavaScript)', + 'type' => 'Authentifizierungsmethode', + ), + 'bdd' => array( + '_' => 'Datenbank', + 'conf' => array( + '_' => 'Datenbank-Konfiguration', + 'ko' => 'Überprüfen Sie Ihre Datenbank-Information.', + 'ok' => 'Datenbank-Konfiguration ist gespeichert worden.', + ), + 'host' => 'Host', + 'prefix' => 'Tabellen-Präfix', + 'password' => 'HTTP-Password', + 'type' => 'Datenbank-Typ', + 'username' => 'HTTP-Nutzername', + ), + 'check' => array( + '_' => 'Überprüfungen', + 'cache' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/cache. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/cache sind in Ordnung.', + ), + 'ctype' => array( + 'nok' => 'Ihnen fehlt eine benötigte Bibliothek für die Überprüfung von Zeichentypen (php-ctype).', + 'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).', + ), + 'curl' => array( + 'nok' => 'Ihnen fehlt cURL (Paket php5-curl).', + 'ok' => 'Sie haben die cURL-Erweiterung.', + ), + 'data' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data sind in Ordnung.', + ), + 'dom' => array( + 'nok' => 'Ihnen fehlt eine benötigte Bibliothek um DOM zu durchstöbern (Paket php-xml).', + 'ok' => 'Sie haben die benötigte Bibliothek um DOM zu durchstöbern.', + ), + 'favicons' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/favicons. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/favicons sind in Ordnung.', + ), + 'http_referer' => array( + 'nok' => 'Bitte stellen Sie sicher, dass Sie Ihren HTTP REFERER nicht abändern.', + 'ok' => 'Ihr HTTP REFERER ist bekannt und entspricht Ihrem Server.', + ), + 'minz' => array( + 'nok' => 'Ihnen fehlt das Minz-Framework.', + 'ok' => 'Sie haben das Minz-Framework.', + ), + 'pcre' => array( + 'nok' => 'Ihnen fehlt eine benötigte Bibliothek für reguläre Ausdrücke (php-pcre).', + '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).', + ), + 'persona' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/persona. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/persona sind in Ordnung.', + ), + 'php' => array( + 'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.', + 'ok' => 'Ihre PHP-Version ist %s, welche kompatibel mit FreshRSS ist.', + ), + 'users' => array( + 'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses ./data/users. Der HTTP-Server muss Schreibrechte besitzen.', + 'ok' => 'Berechtigungen des Verzeichnisses ./data/users sind in Ordnung.', + ), + ), + 'conf' => array( + '_' => 'Allgemeine Konfiguration', + 'ok' => 'Allgemeine Konfiguration ist gespeichert worden.', + ), + 'congratulations' => 'Glückwunsch!', + 'default_user' => 'Nutzername des Standardbenutzers (maximal 16 alphanumerische Zeichen)', + 'delete_articles_after' => 'Entferne Artikel nach', + 'fix_errors_before' => 'Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.', + 'javascript_is_better' => 'FreshRSS ist ansprechender mit aktiviertem JavaScript', + 'language' => array( + '_' => 'Sprache', + 'choose' => 'Wählen Sie eine Sprache für FreshRSS', + 'defined' => 'Sprache ist festgelegt worden.', + ), + 'not_deleted' => 'Etwas ist schiefgelaufen; Sie müssen die Datei %s manuell löschen.', + 'ok' => 'Der Installationsvorgang war erfolgreich.', + 'step' => 'Schritt %d', + 'steps' => 'Schritte', + 'title' => 'Installation · FreshRSS', + 'this_is_the_end' => 'Das ist das Ende', +); diff --git a/sources/app/i18n/de/sub.php b/sources/app/i18n/de/sub.php new file mode 100755 index 0000000..0479b8f --- /dev/null +++ b/sources/app/i18n/de/sub.php @@ -0,0 +1,61 @@ + array( + '_' => 'Kategorie', + 'add' => 'Eine Kategorie hinzufügen', + 'empty' => 'Leere Kategorie', + 'new' => 'Neue Kategorie', + ), + 'feed' => array( + 'add' => 'Einen RSS-Feed hinzufügen', + 'advanced' => 'Erweitert', + 'archiving' => 'Archivierung', + 'auth' => array( + 'configuration' => 'Anmelden', + 'help' => 'Die Verbindung erlaubt Zugriff auf HTTP-geschützte RSS-Feeds', + 'http' => 'HTTP-Authentifizierung', + 'password' => 'HTTP-Passwort', + 'username' => 'HTTP-Nutzername', + ), + 'css_help' => 'Ruft gekürzte RSS-Feeds ab (Achtung, benötigt mehr Zeit!)', + 'css_path' => 'Pfad zur CSS-Datei des Artikels auf der Original-Webseite', + 'description' => 'Beschreibung', + 'empty' => 'Dieser Feed ist leer. Bitte stellen Sie sicher, dass er noch gepflegt wird.', + 'error' => 'Dieser Feed ist auf ein Problem gestoßen. Bitte stellen Sie sicher, dass er immer lesbar ist und aktualisieren Sie ihn dann.', + 'in_main_stream' => 'In Haupt-Feeds zeigen', + 'informations' => 'Information', + 'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird', + 'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie %s eingefügt.', + 'no_selected' => 'Kein Feed ausgewählt.', + 'number_entries' => '%d Artikel', + 'stats' => 'Statistiken', + 'think_to_add' => 'Sie können Feeds hinzufügen.', + 'title' => 'Titel', + 'title_add' => 'Einen RSS-Feed hinzufügen', + 'ttl' => 'Aktualisiere automatisch nicht öfter als', + 'url' => 'Feed-URL', + 'validator' => 'Überprüfen Sie die Gültigkeit des Feeds', + 'website' => 'Webseiten-URL', + ), + 'import_export' => array( + 'export' => 'Exportieren', + 'export_opml' => 'Liste der Feeds exportieren (OPML)', + 'export_starred' => 'Ihre Favoriten exportieren', + 'feed_list' => 'Liste von %s Artikeln', + 'file_to_import' => 'Zu importierende Datei
(OPML, JSON oder Zip)', + 'file_to_import_no_zip' => 'Zu importierende Datei
(OPML oder JSON)', + 'import' => 'Importieren', + 'starred_list' => 'Liste der Lieblingsartikel', + 'title' => 'Importieren / Exportieren', + ), + 'menu' => array( + 'bookmark' => 'Abonnieren (FreshRSS-Lesezeichen)', + 'import_export' => 'Importieren / Exportieren', + 'subscription_management' => 'Abonnementverwaltung', + ), + 'title' => array( + '_' => 'Abonnementverwaltung', + 'feed_management' => 'Verwaltung der RSS-Feeds', + ), +); diff --git a/sources/app/i18n/en.php b/sources/app/i18n/en.php deleted file mode 100755 index 0d36547..0000000 --- a/sources/app/i18n/en.php +++ /dev/null @@ -1,455 +0,0 @@ - 'Login', - 'keep_logged_in' => 'Keep me logged in (1 month)', - 'login_with_persona' => 'Login with Persona', - 'login_persona_problem' => 'Connection problem with Persona?', - 'logout' => 'Logout', - 'search' => 'Search words or #tags', - 'search_short' => 'Search', - - 'configuration' => 'Configuration', - 'users' => 'Users', - 'categories' => 'Categories', - 'category' => 'Category', - 'feed' => 'Feed', - 'feeds' => 'Feeds', - 'shortcuts' => 'Shortcuts', - 'queries' => 'User queries', - 'query_search' => 'Search for "%s"', - 'query_order_asc' => 'Display oldest articles first', - 'query_order_desc' => 'Display newest articles first', - 'query_get_category' => 'Display "%s" category', - 'query_get_feed' => 'Display "%s" feed', - 'query_get_all' => 'Display all articles', - 'query_get_favorite' => 'Display favorite articles', - 'query_state_0' => 'Display all articles', - 'query_state_1' => 'Display read articles', - 'query_state_2' => 'Display unread articles', - 'query_state_3' => 'Display all articles', - 'query_state_4' => 'Display favorite articles', - 'query_state_5' => 'Display read favorite articles', - 'query_state_6' => 'Display unread favorite articles', - 'query_state_7' => 'Display favorite articles', - 'query_state_8' => 'Display not favorite articles', - 'query_state_9' => 'Display read not favorite articles', - 'query_state_10' => 'Display unread not favorite articles', - 'query_state_11' => 'Display not favorite articles', - 'query_state_12' => 'Display all articles', - 'query_state_13' => 'Display read articles', - 'query_state_14' => 'Display unread articles', - 'query_state_15' => 'Display all articles', - 'query_number' => 'Query n°%d', - 'add_query' => 'Add a query', - 'query_created' => 'Query "%s" has been created.', - 'no_query' => 'You haven’t created any user query yet.', - 'query_filter' => 'Filter applied:', - 'no_query_filter' => 'No filter', - 'query_deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.', - 'about' => 'About', - 'stats' => 'Statistics', - 'stats_idle' => 'Idle feeds', - 'stats_main' => 'Main statistics', - 'stats_repartition' => 'Articles repartition', - 'stats_entry_per_hour' => 'Per hour', - 'stats_entry_per_day_of_week' => 'Per day of week', - 'stats_entry_per_month' => 'Per month', - 'stats_percent_of_total' => '%% of total', - - 'last_week' => 'Last week', - 'last_month' => 'Last month', - 'last_3_month' => 'Last three months', - 'last_6_month' => 'Last six months', - 'last_year' => 'Last year', - - 'your_rss_feeds' => 'Your RSS feeds', - 'add_rss_feed' => 'Add a RSS feed', - 'no_rss_feed' => 'No RSS feed', - 'import_export' => 'Import / export', - 'bookmark' => 'Subscribe (FreshRSS bookmark)', - - 'subscription_management' => 'Subscriptions management', - 'main_stream' => 'Main stream', - 'all_feeds' => 'All feeds', - 'favorite_feeds' => 'Favourites (%s)', - 'not_read' => '%d unread', - 'not_reads' => '%d unread', - - 'filter' => 'Filter', - 'see_website' => 'See website', - 'administration' => 'Manage', - 'actualize' => 'Actualize', - - 'mark_read' => 'Mark as read', - 'mark_favorite' => 'Mark as favourite', - 'mark_all_read' => 'Mark all as read', - 'mark_feed_read' => 'Mark feed as read', - 'mark_cat_read' => 'Mark category as read', - 'before_one_day' => 'Before one day', - 'before_one_week' => 'Before one week', - 'display' => 'Display', - 'normal_view' => 'Normal view', - 'reader_view' => 'Reading view', - 'global_view' => 'Global view', - 'rss_view' => 'RSS feed', - 'show_all_articles' => 'Show all articles', - 'show_not_reads' => 'Show only unread', - 'show_adaptive' => 'Adjust showing', - 'show_read' => 'Show only read', - 'show_favorite' => 'Show only favorites', - 'show_not_favorite' => 'Show all but favorites', - 'older_first' => 'Oldest first', - 'newer_first' => 'Newer first', - - // Pagination - 'first' => 'First', - 'previous' => 'Previous', - 'next' => 'Next', - 'last' => 'Last', - - // CONTROLLERS - 'article_published_on' => 'This article originally appeared on %s', - 'article_published_on_author' => 'This article originally appeared on %s by %s', - - 'access_denied' => 'You don’t have permission to access this page', - 'page_not_found' => 'You are looking for a page which doesn’t exist', - 'error_occurred' => 'An error occurred', - 'error_occurred_update' => 'Nothing was changed', - - 'default_category' => 'Uncategorized', - 'categories_updated' => 'Categories have been updated', - 'categories_management' => 'Categories management', - 'feed_updated' => 'Feed has been updated', - 'rss_feed_management' => 'RSS feeds management', - 'configuration_updated' => 'Configuration has been updated', - 'sharing_management' => 'Sharing options management', - 'bad_opml_file' => 'Your OPML file is invalid', - 'shortcuts_updated' => 'Shortcuts have been updated', - 'shortcuts_navigation' => 'Navigation', - 'shortcuts_navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.
With the "Alt" modifier, navigation shortcuts apply on categories.', - 'shortcuts_article_action' => 'Article actions', - 'shortcuts_other_action' => 'Other actions', - 'feeds_marked_read' => 'Feeds have been marked as read', - 'updated' => 'Modifications have been updated', - - 'already_subscribed' => 'You have already subscribed to %s', - 'feed_added' => 'RSS feed %s has been added', - 'feed_not_added' => '%s could not be added', - 'internal_problem_feed' => 'The RSS feed could not be added. Check FressRSS logs for details.', - 'invalid_url' => 'URL %s is invalid', - 'feed_actualized' => '%s has been updated', - 'n_feeds_actualized' => '%d feeds have been updated', - 'feeds_actualized' => 'RSS feeds have been updated', - 'no_feed_actualized' => 'No RSS feed has been updated', - 'n_entries_deleted' => '%d articles have been deleted', - 'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred', - 'feeds_imported' => 'Your feeds have been imported and will now be updated', - 'category_emptied' => 'Category has been emptied', - 'feed_deleted' => 'Feed has been deleted', - 'feed_validator' => 'Check the validity of the feed', - - 'optimization_complete' => 'Optimization complete', - - 'your_rss_feeds' => 'Your RSS feeds', - 'your_favorites' => 'Your favourites', - 'public' => 'Public', - 'invalid_login' => 'Login is invalid', - - 'file_is_nok' => 'Check permissions on %s directory. HTTP server must have rights to write into.', - - // VIEWS - 'save' => 'Save', - 'delete' => 'Delete', - 'cancel' => 'Cancel', - 'submit' => 'Submit', - - 'back_to_rss_feeds' => '← Go back to your RSS feeds', - 'feeds_moved_category_deleted' => 'When you delete a category, their feeds are automatically classified under %s.', - 'category_number' => 'Category n°%d', - 'ask_empty' => 'Clear?', - 'number_feeds' => '%d feeds', - 'can_not_be_deleted' => 'Cannot be deleted', - 'add_category' => 'Add a category', - 'new_category' => 'New category', - - 'javascript_for_shortcuts' => 'JavaScript must be enabled in order to use shortcuts', - 'javascript_should_be_activated'=> 'JavaScript must be enabled', - 'shift_for_all_read' => '+ shift to mark all articles as read', - 'see_on_website' => 'See on original website', - 'next_article' => 'Skip to the next article', - 'last_article' => 'Skip to the last article', - 'previous_article' => 'Skip to the previous article', - 'first_article' => 'Skip to the first article', - 'next_page' => 'Skip to the next page', - 'previous_page' => 'Skip to the previous page', - 'collapse_article' => 'Collapse', - 'auto_share' => 'Share', - 'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.', - 'focus_search' => 'Access search box', - 'user_filter' => 'Access user filters', - 'user_filter_help' => 'If there is only one user filter, it is used. Else filters are accessible by their number.', - 'help' => 'Display documentation', - - 'file_to_import' => 'File to import
(OPML, Json or Zip)', - 'file_to_import_no_zip' => 'File to import
(OPML or Json)', - 'import' => 'Import', - 'file_cannot_be_uploaded' => 'File cannot be uploaded!', - 'zip_error' => 'An error occured during Zip import.', - 'no_zip_extension' => 'Zip extension is not present on your server.', - 'export' => 'Export', - 'export_opml' => 'Export list of feeds (OPML)', - 'export_starred' => 'Export your favourites', - 'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.', - 'starred_list' => 'List of favourite articles', - 'feed_list' => 'List of %s articles', - 'or' => 'or', - - 'informations' => 'Information', - 'damn' => 'Damn!', - 'ok' => 'Ok!', - 'attention' => 'Be careful!', - 'feed_in_error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', - 'feed_empty' => 'This feed is empty. Please verify that it is still maintained.', - 'feed_description' => 'Description', - 'website_url' => 'Website URL', - 'feed_url' => 'Feed URL', - 'articles' => 'articles', - 'number_articles' => '%d articles', - 'by_feed' => 'by feed', - 'by_default' => 'By default', - 'keep_history' => 'Minimum number of articles to keep', - 'ttl' => 'Do not automatically refresh more often than', - 'categorize' => 'Store in a category', - 'truncate' => 'Delete all articles', - 'advanced' => 'Advanced', - 'show_in_all_flux' => 'Show in main stream', - 'yes' => 'Yes', - 'no' => 'No', - 'css_path_on_website' => 'Articles CSS path on original website', - 'retrieve_truncated_feeds' => 'Retrieves truncated RSS feeds (attention, requires more time!)', - 'http_authentication' => 'HTTP Authentication', - 'http_username' => 'HTTP username', - 'http_password' => 'HTTP password', - 'blank_to_disable' => 'Leave blank to disable', - 'share_name' => 'Share name to display', - 'share_url' => 'Share URL to use', - 'not_yet_implemented' => 'Not yet implemented', - 'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds', - 'no_selected_feed' => 'No feed selected.', - 'think_to_add' => 'You may add some feeds.', - - 'current_user' => 'Current user', - 'default_user' => 'Username of the default user (maximum 16 alphanumeric characters)', - 'password_form' => 'Password
(for the Web-form login method)', - 'password_api' => 'Password API
(e.g., for mobile apps)', - 'persona_connection_email' => 'Login mail address
(for Mozilla Persona)', - 'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)', - 'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles', - 'unsafe_autologin' => 'Allow unsafe automatic login using the format: ', - 'api_enabled' => 'Allow API access (required for mobile apps)', - 'auth_token' => 'Authentication token', - 'explain_token' => 'Allows to access RSS output of the default user without authentication.
%s?output=rss&token=%s', - 'login_configuration' => 'Login', - 'is_admin' => 'is administrator', - 'auth_type' => 'Authentication method', - 'auth_none' => 'None (dangerous)', - 'auth_form' => 'Web form (traditional, requires JavaScript)', - 'http_auth' => 'HTTP (for advanced users with HTTPS)', - 'auth_persona' => 'Mozilla Persona (modern, requires JavaScript)', - 'users_list' => 'List of users', - 'create_user' => 'Create new user', - 'username' => 'Username', - 'username_admin' => 'Administrator username', - 'password' => 'Password', - 'create' => 'Create', - 'user_created' => 'User %s has been created', - 'user_deleted' => 'User %s has been deleted', - - 'language' => 'Language', - 'month' => 'months', - 'archiving_configuration' => 'Archiving', - 'delete_articles_every' => 'Remove articles after', - 'purge_now' => 'Purge now', - 'purge_completed' => 'Purge completed (%d articles deleted)', - 'archiving_configuration_help' => 'More options are available in the individual stream settings', - 'reading_configuration' => 'Reading', - 'display_configuration' => 'Display', - 'articles_per_page' => 'Number of articles per page', - 'number_divided_when_reader' => 'Divided by 2 in the reading view.', - 'default_view' => 'Default view', - 'articles_to_display' => 'Articles to display', - 'sort_order' => 'Sort order', - 'auto_load_more' => 'Load next articles at the page bottom', - 'display_articles_unfolded' => 'Show articles unfolded by default', - 'display_categories_unfolded' => 'Show categories folded by default', - 'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)', - 'after_onread' => 'After “mark all as read”,', - 'jump_next' => 'jump to next unread sibling (feed or category)', - 'article_icons' => 'Article icons', - 'top_line' => 'Top line', - 'bottom_line' => 'Bottom line', - 'html5_notif_timeout' => 'HTML5 notification timeout', - 'seconds_(0_means_no_timeout)' => 'seconds (0 means no timeout)', - 'img_with_lazyload' => 'Use "lazy load" mode to load pictures', - 'sticky_post' => 'Stick the article to the top when opened', - 'reading_confirm' => 'Display a confirmation dialog on “mark all as read” actions', - 'auto_read_when' => 'Mark article as read…', - 'article_viewed' => 'when article is viewed', - 'article_open_on_website' => 'when article is opened on its original website', - 'scroll' => 'while scrolling', - 'upon_reception' => 'upon reception of the article', - 'your_shaarli' => 'Your Shaarli', - 'your_wallabag' => 'Your wallabag', - 'your_diaspora_pod' => 'Your Diaspora* pod', - 'sharing' => 'Sharing', - 'share' => 'Share', - 'by_email' => 'By email', - 'optimize_bdd' => 'Optimize database', - 'optimize_todo_sometimes' => 'To do occasionally to reduce the size of the database', - 'theme' => 'Theme', - 'content_width' => 'Content width', - 'width_thin' => 'Thin', - 'width_medium' => 'Medium', - 'width_large' => 'Large', - 'width_no_limit' => 'No limit', - 'more_information' => 'More information', - 'activate_sharing' => 'Activate sharing', - 'shaarli' => 'Shaarli', - 'blogotext' => 'Blogotext', - 'wallabag' => 'wallabag', - 'diaspora' => 'Diaspora*', - 'twitter' => 'Twitter', - 'g+' => 'Google+', - 'facebook' => 'Facebook', - 'email' => 'Email', - 'print' => 'Print', - - 'article' => 'Article', - 'title' => 'Title', - 'author' => 'Author', - 'publication_date' => 'Date of publication', - 'by' => 'by', - - 'load_more' => 'Load more articles', - 'nothing_to_load' => 'There are no more articles', - - 'rss_feeds_of' => 'RSS feed of %s', - - 'refresh' => 'Refresh', - 'no_feed_to_refresh' => 'There is no feed to refresh…', - - 'today' => 'Today', - 'yesterday' => 'Yesterday', - 'before_yesterday' => 'Before yesterday', - 'new_article' => 'There are new available articles, click to refresh the page.', - 'by_author' => 'By %s', - 'related_tags' => 'Related tags', - 'no_feed_to_display' => 'There is no article to show.', - - 'about_freshrss' => 'About FreshRSS', - 'project_website' => 'Project website', - 'lead_developer' => 'Lead developer', - 'website' => 'Website', - 'bugs_reports' => 'Bugs reports', - 'github_or_email' => 'on Github or by mail', - 'license' => 'License', - 'agpl3' => 'AGPL 3', - 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like Kriss Feed or Leed. It is light and easy to take in hand while being powerful and configurable tool.', - 'credits' => 'Credits', - 'credits_content' => 'Some design elements come from Bootstrap although FreshRSS doesn’t use this framework. Icons come from GNOME project. Open Sans font police has been created by Steve Matteson. Favicons are collected with getFavicon API. FreshRSS is based on Minz, a PHP framework.', - 'version' => 'Version', - - 'logs' => 'Logs', - 'logs_empty' => 'Log file is empty', - 'clear_logs' => 'Clear the logs', - - 'forbidden_access' => 'Access is forbidden!', - 'login_required' => 'Login required:', - - 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', - 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You may lost related favorites and user queries. It cannot be cancelled!', - 'notif_title_new_articles' => 'FreshRSS: new articles!', - 'notif_body_new_articles' => 'There are \d new articles to read on FreshRSS.', - - // DATE - 'january' => 'January', - 'february' => 'February', - 'march' => 'March', - 'april' => 'April', - 'may' => 'May', - 'june' => 'June', - 'july' => 'July', - 'august' => 'August', - 'september' => 'September', - 'october' => 'October', - 'november' => 'November', - 'december' => 'December', - 'january' => 'Jan', - 'february' => 'Feb', - 'march' => 'Mar', - 'april' => 'Apr', - 'may' => 'May', - 'june' => 'Jun', - 'july' => 'Jul', - 'august' => 'Aug', - 'september' => 'Sep', - 'october' => 'Oct', - 'november' => 'Nov', - 'december' => 'Dec', - 'sun' => 'Sun', - 'mon' => 'Mon', - 'tue' => 'Tue', - 'wed' => 'Wed', - 'thu' => 'Thu', - 'fri' => 'Fri', - 'sat' => 'Sat', - // special format for date() function - 'Jan' => '\J\a\n\u\a\r\y', - 'Feb' => '\F\e\b\r\u\a\r\y', - 'Mar' => '\M\a\r\c\h', - 'Apr' => '\A\p\r\i\l', - 'May' => '\M\a\y', - 'Jun' => '\J\u\n\e', - 'Jul' => '\J\u\l\y', - 'Aug' => '\A\u\g\u\s\t', - 'Sep' => '\S\e\p\t\e\m\b\e\r', - 'Oct' => '\O\c\t\o\b\e\r', - 'Nov' => '\N\o\v\e\m\b\e\r', - 'Dec' => '\D\e\c\e\m\b\e\r', - // format for date() function, %s allows to indicate month in letter - 'format_date' => '%s j\<\s\u\p\>S\<\/\s\u\p\> Y', - 'format_date_hour' => '%s j\<\s\u\p\>S\<\/\s\u\p\> Y \a\t H\:i', - - 'status_favorites' => 'Favourites', - 'status_read' => 'Read', - 'status_unread' => 'Unread', - 'status_total' => 'Total', - - 'stats_entry_repartition' => 'Entries repartition', - 'stats_entry_per_day' => 'Entries per day (last 30 days)', - 'stats_feed_per_category' => 'Feeds per category', - 'stats_entry_per_category' => 'Entries per category', - 'stats_top_feed' => 'Top ten feeds', - 'stats_entry_count' => 'Entry count', - 'stats_no_idle' => 'There is no idle feed!', - - 'update' => 'Update', - 'update_system' => 'Update system', - 'update_check' => 'Check for new updates', - 'update_last' => 'Last verification: %s', - 'update_can_apply' => 'An update is available.', - 'update_apply' => 'Apply', - 'update_server_not_found' => 'Update server cannot be found. [%s]', - 'no_update' => 'No update to apply', - 'update_problem' => 'The update process has encountered an error: %s', - 'update_finished' => 'Update completed!', - - 'auth_reset' => 'Authentication reset', - 'auth_will_reset' => 'Authentication system will be reset: a form will be used instead of Persona.', - 'auth_not_persona' => 'Only Persona system can be reset.', - 'auth_no_password_set' => 'Administrator password hasn’t been set. This feature isn’t available.', - 'auth_form_set' => 'Form is now your default authentication system.', - 'auth_form_not_set' => 'A problem occured during authentication system configuration. Please retry later.', -); diff --git a/sources/app/i18n/en/admin.php b/sources/app/i18n/en/admin.php new file mode 100755 index 0000000..d2fcd3e --- /dev/null +++ b/sources/app/i18n/en/admin.php @@ -0,0 +1,170 @@ + array( + 'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)', + 'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles', + 'api_enabled' => 'Allow API access (required for mobile apps)', + 'form' => 'Web form (traditional, requires JavaScript)', + 'http' => 'HTTP (for advanced users with HTTPS)', + 'none' => 'None (dangerous)', + 'persona' => 'Mozilla Persona (modern, requires JavaScript)', + 'title' => 'Authentication', + 'title_reset' => 'Authentication reset', + 'token' => 'Authentication token', + 'token_help' => 'Allows to access RSS output of the default user without authentication:', + 'type' => 'Authentication method', + 'unsafe_autologin' => 'Allow unsafe automatic login using the format: ', + ), + 'check_install' => array( + 'cache' => array( + 'nok' => 'Check permissions on ./data/cache directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on cache directory are good.', + ), + 'categories' => array( + 'nok' => 'Category table is bad configured.', + 'ok' => 'Category table is ok.', + ), + 'connection' => array( + 'nok' => 'Connection to the database cannot being established.', + 'ok' => 'Connection to the database is ok.', + ), + 'ctype' => array( + 'nok' => 'You lack a required library for character type checking (php-ctype).', + 'ok' => 'You have the required library for character type checking (ctype).', + ), + 'curl' => array( + 'nok' => 'You lack cURL (php5-curl package).', + 'ok' => 'You have cURL extension.', + ), + 'data' => array( + 'nok' => 'Check permissions on ./data directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on data directory are good.', + ), + 'database' => 'Database installation', + 'dom' => array( + 'nok' => 'You lack a required library to browse the DOM (php-xml package).', + 'ok' => 'You have the required library to browse the DOM.', + ), + 'entries' => array( + 'nok' => 'Entry table is bad configured.', + 'ok' => 'Entry table is ok.', + ), + 'favicons' => array( + 'nok' => 'Check permissions on ./data/favicons directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on favicons directory are good.', + ), + 'feeds' => array( + 'nok' => 'Feed table is bad configured.', + 'ok' => 'Feed table is ok.', + ), + 'files' => 'File installation', + 'json' => array( + 'nok' => 'You lack JSON (php5-json package).', + 'ok' => 'You have JSON extension.', + ), + 'minz' => array( + 'nok' => 'You lack the Minz framework.', + 'ok' => 'You have the Minz framework.', + ), + 'pcre' => array( + 'nok' => 'You lack a required library for regular expressions (php-pcre).', + 'ok' => 'You have the required library for regular expressions (PCRE).', + ), + 'pdo' => array( + 'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).', + 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Check permissions on ./data/persona directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on Mozilla Persona directory are good.', + ), + 'php' => array( + '_' => 'PHP installation', + 'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.', + 'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.', + ), + 'tables' => array( + 'nok' => 'There is one or more lacking tables in the database.', + 'ok' => 'Tables are existing in the database.', + ), + 'title' => 'Installation checking', + 'tokens' => array( + 'nok' => 'Check permissions on ./data/tokens directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on tokens directory are good.', + ), + 'users' => array( + 'nok' => 'Check permissions on ./data/users directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on users directory are good.', + ), + 'zip' => array( + 'nok' => 'You lack ZIP extension (php5-zip package).', + 'ok' => 'You have ZIP extension.', + ), + ), + 'extensions' => array( + 'disabled' => 'Disabled', + 'empty_list' => 'There is no installed extension', + 'enabled' => 'Enabled', + 'no_configure_view' => 'This extension cannot be configured.', + 'system' => array( + '_' => 'System extensions', + 'no_rights' => 'System extension (you have no rights on it)', + ), + 'title' => 'Extensions', + 'user' => 'User extensions', + ), + 'stats' => array( + '_' => 'Statistics', + 'all_feeds' => 'All feeds', + 'category' => 'Category', + 'entry_count' => 'Entry count', + 'entry_per_category' => 'Entries per category', + 'entry_per_day' => 'Entries per day (last 30 days)', + 'entry_per_day_of_week' => 'Per day of week (average: %.2f messages)', + 'entry_per_hour' => 'Per hour (average: %.2f messages)', + 'entry_per_month' => 'Per month (average: %.2f messages)', + 'entry_repartition' => 'Entries repartition', + 'feed' => 'Feed', + 'feed_per_category' => 'Feeds per category', + 'idle' => 'Idle feeds', + 'main' => 'Main statistics', + 'main_stream' => 'Main stream', + 'menu' => array( + 'idle' => 'Idle feeds', + 'main' => 'Main statistics', + 'repartition' => 'Articles repartition', + ), + 'no_idle' => 'There is no idle feed!', + 'number_entries' => '%d articles', + 'percent_of_total' => '%% of total', + 'repartition' => 'Articles repartition', + 'status_favorites' => 'Favourites', + 'status_read' => 'Read', + 'status_total' => 'Total', + 'status_unread' => 'Unread', + 'title' => 'Statistics', + 'top_feed' => 'Top ten feeds', + ), + 'update' => array( + '_' => 'Update system', + 'apply' => 'Apply', + 'check' => 'Check for new updates', + 'current_version' => 'Your current version of FreshRSS is the %s.', + 'last' => 'Last verification: %s', + 'none' => 'No update to apply', + 'title' => 'Update system', + ), + 'user' => array( + 'articles_and_size' => '%s articles (%s)', + 'create' => 'Create new user', + 'email_persona' => 'Login mail address
(for Mozilla Persona)', + 'language' => 'Language', + 'password_form' => 'Password
(for the Web-form login method)', + 'password_format' => 'At least 7 characters', + 'title' => 'Manage users', + 'user_list' => 'List of users', + 'username' => 'Username', + 'users' => 'Users', + ), +); diff --git a/sources/app/i18n/en/conf.php b/sources/app/i18n/en/conf.php new file mode 100755 index 0000000..308c45d --- /dev/null +++ b/sources/app/i18n/en/conf.php @@ -0,0 +1,169 @@ + array( + '_' => 'Archiving', + 'advanced' => 'Advanced', + 'delete_after' => 'Remove articles after', + 'help' => 'More options are available in the individual stream settings', + 'keep_history_by_feed' => 'Minimum number of articles to keep by feed', + 'optimize' => 'Optimize database', + 'optimize_help' => 'To do occasionally to reduce the size of the database', + 'purge_now' => 'Purge now', + 'title' => 'Archiving', + 'ttl' => 'Do not automatically refresh more often than', + ), + 'display' => array( + '_' => 'Display', + 'icon' => array( + 'bottom_line' => 'Bottom line', + 'entry' => 'Article icons', + 'publication_date' => 'Date of publication', + 'related_tags' => 'Related tags', + 'sharing' => 'Sharing', + 'top_line' => 'Top line', + ), + 'language' => 'Language', + 'notif_html5' => array( + 'seconds' => 'seconds (0 means no timeout)', + 'timeout' => 'HTML5 notification timeout', + ), + 'theme' => 'Theme', + 'title' => 'Display', + 'width' => array( + 'content' => 'Content width', + 'large' => 'Large', + 'medium' => 'Medium', + 'no_limit' => 'No limit', + 'thin' => 'Thin', + ), + ), + 'query' => array( + '_' => 'User queries', + 'deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.', + 'filter' => 'Filter applied:', + 'get_all' => 'Display all articles', + 'get_category' => 'Display "%s" category', + 'get_favorite' => 'Display favorite articles', + 'get_feed' => 'Display "%s" feed', + 'no_filter' => 'No filter', + 'none' => 'You haven’t created any user query yet.', + 'number' => 'Query n°%d', + 'order_asc' => 'Display oldest articles first', + 'order_desc' => 'Display newest articles first', + 'search' => 'Search for "%s"', + 'state_0' => 'Display all articles', + 'state_1' => 'Display read articles', + 'state_2' => 'Display unread articles', + 'state_3' => 'Display all articles', + 'state_4' => 'Display favorite articles', + 'state_5' => 'Display read favorite articles', + 'state_6' => 'Display unread favorite articles', + 'state_7' => 'Display favorite articles', + 'state_8' => 'Display not favorite articles', + 'state_9' => 'Display read not favorite articles', + 'state_10' => 'Display unread not favorite articles', + 'state_11' => 'Display not favorite articles', + 'state_12' => 'Display all articles', + 'state_13' => 'Display read articles', + 'state_14' => 'Display unread articles', + 'state_15' => 'Display all articles', + 'title' => 'User queries', + ), + 'profile' => array( + '_' => 'Profile management', + 'email_persona' => 'Login mail address
(for Mozilla Persona)', + 'password_api' => 'Password API
(e.g., for mobile apps)', + 'password_form' => 'Password
(for the Web-form login method)', + 'password_format' => 'At least 7 characters', + 'title' => 'Profile', + ), + 'reading' => array( + '_' => 'Reading', + 'after_onread' => 'After “mark all as read”,', + 'articles_per_page' => 'Number of articles per page', + 'auto_load_more' => 'Load next articles at the page bottom', + 'auto_remove_article' => 'Hide articles after reading', + 'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions', + 'display_articles_unfolded' => 'Show articles unfolded by default', + 'display_categories_unfolded' => 'Show categories folded by default', + 'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)', + 'img_with_lazyload' => 'Use "lazy load" mode to load pictures', + 'jump_next' => 'jump to next unread sibling (feed or category)', + 'number_divided_when_reader' => 'Divided by 2 in the reading view.', + 'read' => array( + 'article_open_on_website' => 'when article is opened on its original website', + 'article_viewed' => 'when article is viewed', + 'scroll' => 'while scrolling', + 'upon_reception' => 'upon reception of the article', + 'when' => 'Mark article as read…', + ), + 'show' => array( + '_' => 'Articles to display', + 'adaptive' => 'Adjust showing', + 'all_articles' => 'Show all articles', + 'unread' => 'Show only unread', + ), + 'sort' => array( + '_' => 'Sort order', + 'newer_first' => 'Newer first', + 'older_first' => 'Oldest first', + ), + 'sticky_post' => 'Stick the article to the top when opened', + 'title' => 'Reading', + 'view' => array( + 'default' => 'Default view', + 'global' => 'Global view', + 'normal' => 'Normal view', + 'reader' => 'Reading view', + ), + ), + 'sharing' => array( + '_' => 'Sharing', + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'Email', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'more_information' => 'More information', + 'print' => 'Print', + 'shaarli' => 'Shaarli', + 'share_name' => 'Share name to display', + 'share_url' => 'Share URL to use', + 'title' => 'Sharing', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'shortcut' => array( + '_' => 'Shortcuts', + 'article_action' => 'Article actions', + 'auto_share' => 'Share', + 'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.', + 'close_dropdown' => 'Close menus', + 'collapse_article' => 'Collapse', + 'first_article' => 'Skip to the first article', + 'focus_search' => 'Access search box', + 'help' => 'Display documentation', + 'javascript' => 'JavaScript must be enabled in order to use shortcuts', + 'last_article' => 'Skip to the last article', + 'load_more' => 'Load more articles', + 'mark_read' => 'Mark as read', + 'mark_favorite' => 'Mark as favourite', + 'navigation' => 'Navigation', + 'navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.
With the "Alt" modifier, navigation shortcuts apply on categories.', + 'next_article' => 'Skip to the next article', + 'other_action' => 'Other actions', + 'previous_article' => 'Skip to the previous article', + 'see_on_website' => 'See on original website', + 'shift_for_all_read' => '+ shift to mark all articles as read', + 'title' => 'Shortcuts', + 'user_filter' => 'Access user filters', + 'user_filter_help' => 'If there is only one user filter, it is used. Else filters are accessible by their number.', + ), + 'user' => array( + 'articles_and_size' => '%s articles (%s)', + 'current' => 'Current user', + 'is_admin' => 'is administrator', + 'users' => 'Users', + ), +); diff --git a/sources/app/i18n/en/feedback.php b/sources/app/i18n/en/feedback.php new file mode 100755 index 0000000..19af81e --- /dev/null +++ b/sources/app/i18n/en/feedback.php @@ -0,0 +1,110 @@ + array( + 'optimization_complete' => 'Optimization complete', + ), + 'access' => array( + 'denied' => 'You don’t have permission to access this page', + 'not_found' => 'You are looking for a page which doesn’t exist', + ), + 'auth' => array( + 'form' => array( + 'not_set' => 'A problem occured during authentication system configuration. Please retry later.', + 'set' => 'Form is now your default authentication system.', + ), + 'login' => array( + 'invalid' => 'Login is invalid', + 'success' => 'You are connected', + ), + 'logout' => array( + 'success' => 'You are disconnected', + ), + 'no_password_set' => 'Administrator password hasn’t been set. This feature isn’t available.', + 'not_persona' => 'Only Persona system can be reset.', + ), + 'conf' => array( + 'error' => 'An error occurred during configuration saving', + 'query_created' => 'Query "%s" has been created.', + 'shortcuts_updated' => 'Shortcuts have been updated', + 'updated' => 'Configuration has been updated', + ), + 'extensions' => array( + 'already_enabled' => '%s is already enabled', + 'disable' => array( + 'ko' => '%s cannot be disabled. Check FressRSS logs for details.', + 'ok' => '%s is now disabled', + ), + 'enable' => array( + 'ko' => '%s cannot be enabled. Check FressRSS logs for details.', + 'ok' => '%s is now enabled', + ), + 'no_access' => 'You have no access on %s', + 'not_enabled' => '%s is not enabled yet', + 'not_found' => '%s does not exist', + ), + 'import_export' => array( + '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.', + ), + 'sub' => array( + 'actualize' => 'Actualize', + 'category' => array( + 'created' => 'Category %s has been created.', + 'deleted' => 'Category has been deleted.', + 'emptied' => 'Category has been emptied', + 'error' => 'Category cannot be updated', + 'name_exists' => 'Category name already exists.', + 'no_id' => 'You must precise the id of the category.', + 'no_name' => 'Category name cannot be empty.', + 'not_delete_default' => 'You cannot delete the default category!', + 'not_exist' => 'The category does not exist!', + 'over_max' => 'You have reached your limit of categories (%d)', + 'updated' => 'Category has been updated.', + ), + 'feed' => array( + 'actualized' => '%s has been updated', + 'actualizeds' => 'RSS feeds have been updated', + 'added' => 'RSS feed %s has been added', + 'already_subscribed' => 'You have already subscribed to %s', + 'deleted' => 'Feed has been deleted', + 'error' => 'Feed cannot be updated', + 'internal_problem' => 'The RSS feed could not be added. Check FressRSS logs for details.', + 'invalid_url' => 'URL %s is invalid', + 'marked_read' => 'Feeds have been marked as read', + 'n_actualized' => '%d feeds have been updated', + 'n_entries_deleted' => '%d articles have been deleted', + 'no_refresh' => 'There is no feed to refresh…', + 'not_added' => '%s could not be added', + 'over_max' => 'You have reached your limit of feeds (%d)', + 'updated' => 'Feed has been updated', + ), + 'purge_completed' => 'Purge completed (%d articles deleted)', + ), + 'update' => array( + 'can_apply' => 'FreshRSS will be now updated to the version %s.', + 'error' => 'The update process has encountered an error: %s', + 'file_is_nok' => 'Check permissions on %s directory. HTTP server must have rights to write into', + 'finished' => 'Update completed!', + 'none' => 'No update to apply', + 'server_not_found' => 'Update server cannot be found. [%s]', + ), + 'user' => array( + 'created' => array( + '_' => 'User %s has been created', + 'error' => 'User %s cannot be created', + ), + 'deleted' => array( + '_' => 'User %s has been deleted', + 'error' => 'User %s cannot be deleted', + ), + ), + 'profile' => array( + 'error' => 'Your profile cannot be modified', + 'updated' => 'Your profile has been modified', + ), +); diff --git a/sources/app/i18n/en/gen.php b/sources/app/i18n/en/gen.php new file mode 100755 index 0000000..2143822 --- /dev/null +++ b/sources/app/i18n/en/gen.php @@ -0,0 +1,163 @@ + array( + 'actualize' => 'Actualize', + 'back_to_rss_feeds' => '← Go back to your RSS feeds', + 'cancel' => 'Cancel', + 'create' => 'Create', + 'disable' => 'Disable', + 'empty' => 'Empty', + 'enable' => 'Enable', + 'export' => 'Export', + 'filter' => 'Filtrer', + 'import' => 'Import', + 'manage' => 'Manage', + 'mark_read' => 'Mark as read', + 'mark_favorite' => 'Mark as favourite', + 'remove' => 'Remove', + 'see_website' => 'See website', + 'submit' => 'Submit', + 'truncate' => 'Delete all articles', + ), + 'auth' => array( + 'keep_logged_in' => 'Keep me logged in (1 month)', + 'login' => 'Login', + 'login_persona' => 'Login with Persona', + 'login_persona_problem' => 'Connection problem with Persona?', + 'logout' => 'Logout', + 'password' => 'Password', + 'reset' => 'Authentication reset', + 'username' => 'Username', + 'username_admin' => 'Administrator username', + 'will_reset' => 'Authentication system will be reset: a form will be used instead of Persona.', + ), + 'date' => array( + 'Apr' => '\\A\\p\\r\\i\\l', + 'Aug' => '\\A\\u\\g\\u\\s\\t', + 'Dec' => '\\D\\e\\c\\e\\m\\b\\e\\r', + 'Feb' => '\\F\\e\\b\\r\\u\\a\\r\\y', + 'Jan' => '\\J\\a\\n\\u\\a\\r\\y', + 'Jul' => '\\J\\u\\l\\y', + 'Jun' => '\\J\\u\\n\\e', + 'Mar' => '\\M\\a\\r\\c\\h', + 'May' => '\\M\\a\\y', + 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r', + 'Oct' => '\\O\\c\\t\\o\\b\\e\\r', + 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r', + 'apr' => 'apr', + 'april' => 'Apr', + 'aug' => 'aug', + 'august' => 'Aug', + 'before_yesterday' => 'Before yesterday', + 'dec' => 'dec', + 'december' => 'Dec', + 'feb' => 'feb', + 'february' => 'Feb', + 'format_date' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y', + 'format_date_hour' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y \\a\\t H\\:i', + 'fri' => 'Fri', + 'jan' => 'jan', + 'january' => 'Jan', + 'jul' => 'jul', + 'july' => 'Jul', + 'jun' => 'jun', + 'june' => 'Jun', + 'last_3_month' => 'Last three months', + 'last_6_month' => 'Last six months', + 'last_month' => 'Last month', + 'last_week' => 'Last week', + 'last_year' => 'Last year', + 'mar' => 'mar', + 'march' => 'Mar', + 'may' => 'May', + 'mon' => 'Mon', + 'month' => 'months', + 'nov' => 'nov', + 'november' => 'Nov', + 'oct' => 'oct', + 'october' => 'Oct', + 'sat' => 'Sat', + 'sep' => 'sep', + 'september' => 'Sep', + 'sun' => 'Sun', + 'thu' => 'Thu', + 'today' => 'Today', + 'tue' => 'Tue', + 'wed' => 'Wed', + 'yesterday' => 'Yesterday', + ), + 'freshrss' => array( + '_' => 'FreshRSS', + 'about' => 'About FreshRSS', + ), + 'js' => array( + 'category_empty' => 'Empty category', + 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', + 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!', + 'feedback' => array( + 'body_new_articles' => 'There are \\d new articles to read on FreshRSS.', + 'request_failed' => 'A request has failed, it may have been caused by Internet connection problems.', + 'title_new_articles' => 'FreshRSS: new articles!', + ), + 'new_article' => 'There are new available articles, click to refresh the page.', + 'should_be_activated' => 'JavaScript must be enabled', + ), + 'lang' => array( + 'de' => 'Deutsch', + 'en' => 'English', + 'fr' => 'Français', + ), + 'menu' => array( + 'about' => 'About', + 'admin' => 'Administration', + 'archiving' => 'Archiving', + 'authentication' => 'Authentication', + 'check_install' => 'Installation checking', + 'configuration' => 'Configuration', + 'display' => 'Display', + 'extensions' => 'Extensions', + 'logs' => 'Logs', + 'queries' => 'User queries', + 'reading' => 'Reading', + 'search' => 'Search words or #tags', + 'sharing' => 'Sharing', + 'shortcuts' => 'Shortcuts', + 'stats' => 'Statistics', + 'update' => 'Update', + 'user_management' => 'Manage users', + 'user_profile' => 'Profile', + ), + 'pagination' => array( + 'first' => 'First', + 'last' => 'Last', + 'load_more' => 'Load more articles', + 'mark_all_read' => 'Mark all as read', + 'next' => 'Next', + 'nothing_to_load' => 'There are no more articles', + 'previous' => 'Previous', + ), + 'share' => array( + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'Email', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'print' => 'Print', + 'shaarli' => 'Shaarli', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'short' => array( + 'attention' => 'Attention!', + 'blank_to_disable' => 'Leave blank to disable', + 'by_author' => 'By %s', + 'by_default' => 'By default', + 'damn' => 'Damn!', + 'default_category' => 'Uncategorized', + 'no' => 'No', + 'ok' => 'Ok!', + 'or' => 'or', + 'yes' => 'Yes', + ), +); diff --git a/sources/app/i18n/en/index.php b/sources/app/i18n/en/index.php new file mode 100755 index 0000000..80fa3d9 --- /dev/null +++ b/sources/app/i18n/en/index.php @@ -0,0 +1,61 @@ + array( + '_' => 'About', + 'agpl3' => 'AGPL 3', + 'bugs_reports' => 'Bugs reports', + 'credits' => 'Credits', + 'credits_content' => 'Some design elements come from Bootstrap although FreshRSS doesn’t use this framework. Icons come from GNOME project. Open Sans font police has been created by Steve Matteson. Favicons are collected with getFavicon API. FreshRSS is based on Minz, a PHP framework.', + 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like Kriss Feed or Leed. It is light and easy to take in hand while being powerful and configurable tool.', + 'github' => 'on Github', + 'license' => 'License', + 'project_website' => 'Project website', + 'title' => 'About', + 'version' => 'Version', + 'website' => 'Website', + ), + 'feed' => array( + 'add' => 'You may add some feeds.', + 'empty' => 'There is no article to show.', + 'rss_of' => 'RSS feed of %s', + 'title' => 'Your RSS feeds', + 'title_global' => 'Global view', + 'title_fav' => 'Your favourites', + ), + 'log' => array( + '_' => 'Logs', + 'clear' => 'Clear the logs', + 'empty' => 'Log file is empty', + 'title' => 'Logs', + ), + 'menu' => array( + 'about' => 'About FreshRSS', + 'add_query' => 'Add a query', + 'before_one_day' => 'Before one day', + 'before_one_week' => 'Before one week', + 'favorites' => 'Favourites (%s)', + 'global_view' => 'Global view', + 'main_stream' => 'Main stream', + 'mark_all_read' => 'Mark all as read', + 'mark_cat_read' => 'Mark category as read', + 'mark_feed_read' => 'Mark feed as read', + 'newer_first' => 'Newer first', + 'non-starred' => 'Show all but favorites', + 'normal_view' => 'Normal view', + 'older_first' => 'Oldest first', + 'queries' => 'User queries', + 'read' => 'Show only read', + 'reader_view' => 'Reading view', + 'rss_view' => 'RSS feed', + 'search_short' => 'Search', + 'starred' => 'Show only favorites', + 'stats' => 'Statistics', + 'subscription' => 'Subscriptions management', + 'unread' => 'Show only unread', + ), + 'share' => 'Share', + 'tag' => array( + 'related' => 'Related tags', + ), +); diff --git a/sources/app/i18n/en/install.php b/sources/app/i18n/en/install.php new file mode 100755 index 0000000..2bc6bd3 --- /dev/null +++ b/sources/app/i18n/en/install.php @@ -0,0 +1,107 @@ + array( + 'finish' => 'Complete installation', + 'fix_errors_before' => 'Fix errors before skip to the next step.', + 'next_step' => 'Go to the next step', + ), + 'auth' => array( + 'email_persona' => 'Login mail address
(for Mozilla Persona)', + 'form' => 'Web form (traditional, requires JavaScript)', + 'http' => 'HTTP (for advanced users with HTTPS)', + 'none' => 'None (dangerous)', + 'password_form' => 'Password
(for the Web-form login method)', + 'password_format' => 'At least 7 characters', + 'persona' => 'Mozilla Persona (modern, requires JavaScript)', + 'type' => 'Authentication method', + ), + 'bdd' => array( + '_' => 'Database', + 'conf' => array( + '_' => 'Database configuration', + 'ko' => 'Verify your database information.', + 'ok' => 'Database configuration has been saved.', + ), + 'host' => 'Host', + 'prefix' => 'Table prefix', + 'password' => 'HTTP password', + 'type' => 'Type of database', + 'username' => 'HTTP username', + ), + 'check' => array( + '_' => 'Checks', + 'cache' => array( + 'nok' => 'Check permissions on ./data/cache directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on cache directory are good.', + ), + 'ctype' => array( + 'nok' => 'You lack a required library for character type checking (php-ctype).', + 'ok' => 'You have the required library for character type checking (ctype).', + ), + 'curl' => array( + 'nok' => 'You lack cURL (php5-curl package).', + 'ok' => 'You have cURL extension.', + ), + 'data' => array( + 'nok' => 'Check permissions on ./data directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on data directory are good.', + ), + 'dom' => array( + 'nok' => 'You lack a required library to browse the DOM (php-xml package).', + 'ok' => 'You have the required library to browse the DOM.', + ), + 'favicons' => array( + 'nok' => 'Check permissions on ./data/favicons directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on favicons directory are good.', + ), + 'http_referer' => array( + 'nok' => 'Please check that you are not altering your HTTP REFERER.', + 'ok' => 'Your HTTP REFERER is known and corresponds to your server.', + ), + 'minz' => array( + 'nok' => 'You lack the Minz framework.', + 'ok' => 'You have the Minz framework.', + ), + 'pcre' => array( + 'nok' => 'You lack a required library for regular expressions (php-pcre).', + 'ok' => 'You have the required library for regular expressions (PCRE).', + ), + 'pdo' => array( + 'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).', + 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Check permissions on ./data/persona directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on Mozilla Persona directory are good.', + ), + 'php' => array( + 'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.', + 'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.', + ), + 'users' => array( + 'nok' => 'Check permissions on ./data/users directory. HTTP server must have rights to write into', + 'ok' => 'Permissions on users directory are good.', + ), + ), + 'conf' => array( + '_' => 'General configuration', + 'ok' => 'General configuration has been saved.', + ), + 'congratulations' => 'Congratulations!', + 'default_user' => 'Username of the default user (maximum 16 alphanumeric characters)', + 'delete_articles_after' => 'Remove articles after', + 'fix_errors_before' => 'Fix errors before skip to the next step.', + 'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled', + 'language' => array( + '_' => 'Language', + 'choose' => 'Choose a language for FreshRSS', + 'defined' => 'Language has been defined.', + ), + 'not_deleted' => 'Something went wrong; you must delete the file %s manually.', + 'ok' => 'The installation process was successful.', + 'step' => 'step %d', + 'steps' => 'Steps', + 'title' => 'Installation · FreshRSS', + 'this_is_the_end' => 'This is the end', +); diff --git a/sources/app/i18n/en/sub.php b/sources/app/i18n/en/sub.php new file mode 100755 index 0000000..2b62e47 --- /dev/null +++ b/sources/app/i18n/en/sub.php @@ -0,0 +1,61 @@ + array( + '_' => 'Category', + 'add' => 'Add a category', + 'empty' => 'Empty category', + 'new' => 'New category', + ), + 'feed' => array( + 'add' => 'Add a RSS feed', + 'advanced' => 'Advanced', + 'archiving' => 'Archivage', + 'auth' => array( + 'configuration' => 'Login', + 'help' => 'Connection allows to access HTTP protected RSS feeds', + 'http' => 'HTTP Authentication', + 'password' => 'HTTP password', + 'username' => 'HTTP username', + ), + 'css_help' => 'Retrieves truncated RSS feeds (attention, requires more time!)', + 'css_path' => 'Articles CSS path on original website', + 'description' => 'Description', + 'empty' => 'This feed is empty. Please verify that it is still maintained.', + 'error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', + 'in_main_stream' => 'Show in main stream', + 'informations' => 'Information', + 'keep_history' => 'Minimum number of articles to keep', + 'moved_category_deleted' => 'When you delete a category, their feeds are automatically classified under %s.', + 'no_selected' => 'No feed selected.', + 'number_entries' => '%d articles', + 'stats' => 'Statistics', + 'think_to_add' => 'You may add some feeds.', + 'title' => 'Title', + 'title_add' => 'Add a RSS feed', + 'ttl' => 'Do not automatically refresh more often than', + 'url' => 'Feed URL', + 'validator' => 'Check the validity of the feed', + 'website' => 'Website URL', + ), + 'import_export' => array( + 'export' => 'Export', + 'export_opml' => 'Export list of feeds (OPML)', + 'export_starred' => 'Export your favourites', + 'feed_list' => 'List of %s articles', + 'file_to_import' => 'File to import
(OPML, Json or Zip)', + 'file_to_import_no_zip' => 'File to import
(OPML or Json)', + 'import' => 'Import', + 'starred_list' => 'List of favourite articles', + 'title' => 'Import / export', + ), + 'menu' => array( + 'bookmark' => 'Subscribe (FreshRSS bookmark)', + 'import_export' => 'Import / export', + 'subscription_management' => 'Subscriptions management', + ), + 'title' => array( + '_' => 'Subscriptions management', + 'feed_management' => 'RSS feeds management', + ), +); diff --git a/sources/app/i18n/fr.php b/sources/app/i18n/fr.php deleted file mode 100755 index c72fc3e..0000000 --- a/sources/app/i18n/fr.php +++ /dev/null @@ -1,455 +0,0 @@ - 'Connexion', - 'keep_logged_in' => 'Rester connecté (1 mois)', - 'login_with_persona' => 'Connexion avec Persona', - 'login_persona_problem' => 'Problème de connexion à Persona ?', - 'logout' => 'Déconnexion', - 'search' => 'Rechercher des mots ou des #tags', - 'search_short' => 'Rechercher', - - 'configuration' => 'Configuration', - 'users' => 'Utilisateurs', - 'categories' => 'Catégories', - 'category' => 'Catégorie', - 'feed' => 'Flux', - 'feeds' => 'Flux', - 'shortcuts' => 'Raccourcis', - 'queries' => 'Filtres utilisateurs', - 'query_search' => 'Recherche de "%s"', - 'query_order_asc' => 'Afficher les articles les plus anciens en premier', - 'query_order_desc' => 'Afficher les articles les plus récents en premier', - 'query_get_category' => 'Afficher la catégorie "%s"', - 'query_get_feed' => 'Afficher le flux "%s"', - 'query_get_all' => 'Afficher tous les articles', - 'query_get_favorite' => 'Afficher les articles favoris', - 'query_state_0' => 'Afficher tous les articles', - 'query_state_1' => 'Afficher les articles lus', - 'query_state_2' => 'Afficher les articles non lus', - 'query_state_3' => 'Afficher tous les articles', - 'query_state_4' => 'Afficher les articles favoris', - 'query_state_5' => 'Afficher les articles lus et favoris', - 'query_state_6' => 'Afficher les articles non lus et favoris', - 'query_state_7' => 'Afficher les articles favoris', - 'query_state_8' => 'Afficher les articles non favoris', - 'query_state_9' => 'Afficher les articles lus et non favoris', - 'query_state_10' => 'Afficher les articles non lus et non favoris', - 'query_state_11' => 'Afficher les articles non favoris', - 'query_state_12' => 'Afficher tous les articles', - 'query_state_13' => 'Afficher les articles lus', - 'query_state_14' => 'Afficher les articles non lus', - 'query_state_15' => 'Afficher tous les articles', - 'query_number' => 'Filtre n°%d', - 'add_query' => 'Créer un filtre', - 'query_created' => 'Le filtre "%s" a bien été créé.', - 'no_query' => 'Vous n’avez pas encore créé de filtre.', - 'query_filter' => 'Filtres appliqués :', - 'no_query_filter' => 'Aucun filtre appliqué', - 'query_deprecated' => 'Ce filtre n’est plus valide. La catégorie ou le flux concerné a été supprimé.', - 'about' => 'À propos', - 'stats' => 'Statistiques', - 'stats_idle' => 'Flux inactifs', - 'stats_main' => 'Statistiques principales', - 'stats_repartition' => 'Répartition des articles', - 'stats_entry_per_hour' => 'Par heure', - 'stats_entry_per_day_of_week' => 'Par jour de la semaine', - 'stats_entry_per_month' => 'Par mois', - 'stats_percent_of_total' => '%% du total', - - 'last_week' => 'Depuis la semaine dernière', - 'last_month' => 'Depuis le mois dernier', - 'last_3_month' => 'Depuis les trois derniers mois', - 'last_6_month' => 'Depuis les six derniers mois', - 'last_year' => 'Depuis l’année dernière', - - 'your_rss_feeds' => 'Vos flux RSS', - 'add_rss_feed' => 'Ajouter un flux RSS', - 'no_rss_feed' => 'Aucun flux RSS', - 'import_export' => 'Importer / exporter', - 'bookmark' => 'S’abonner (bookmark FreshRSS)', - - 'subscription_management' => 'Gestion des abonnements', - 'main_stream' => 'Flux principal', - 'all_feeds' => 'Tous les flux', - 'favorite_feeds' => 'Favoris (%s)', - 'not_read' => '%d non lu', - 'not_reads' => '%d non lus', - - 'filter' => 'Filtrer', - 'see_website' => 'Voir le site', - 'administration' => 'Gérer', - 'actualize' => 'Actualiser', - - 'mark_read' => 'Marquer comme lu', - 'mark_favorite' => 'Mettre en favori', - 'mark_all_read' => 'Tout marquer comme lu', - 'mark_feed_read' => 'Marquer le flux comme lu', - 'mark_cat_read' => 'Marquer la catégorie comme lue', - 'before_one_day' => 'Antérieurs à 1 jour', - 'before_one_week' => 'Antérieurs à 1 semaine', - 'display' => 'Affichage', - 'normal_view' => 'Vue normale', - 'reader_view' => 'Vue lecture', - 'global_view' => 'Vue globale', - 'rss_view' => 'Flux RSS', - 'show_all_articles' => 'Afficher tous les articles', - 'show_not_reads' => 'Afficher les non lus', - 'show_adaptive' => 'Adapter l’affichage', - 'show_read' => 'Afficher les lus', - 'show_favorite' => 'Afficher les favoris', - 'show_not_favorite' => 'Afficher tout sauf les favoris', - 'older_first' => 'Plus anciens en premier', - 'newer_first' => 'Plus récents en premier', - - // Pagination - 'first' => 'Début', - 'previous' => 'Précédent', - 'next' => 'Suivant', - 'last' => 'Fin', - - // CONTROLLERS - 'article_published_on' => 'Article publié initialement sur %s', - 'article_published_on_author' => 'Article publié initialement sur %s par %s', - - 'access_denied' => 'Vous n’avez pas le droit d’accéder à cette page !', - 'page_not_found' => 'La page que vous cherchez n’existe pas !', - 'error_occurred' => 'Une erreur est survenue !', - 'error_occurred_update' => 'Rien n’a été modifié !', - - 'default_category' => 'Sans catégorie', - 'categories_updated' => 'Les catégories ont été mises à jour.', - 'categories_management' => 'Gestion des catégories', - 'feed_updated' => 'Le flux a été mis à jour.', - 'rss_feed_management' => 'Gestion des flux RSS', - 'configuration_updated' => 'La configuration a été mise à jour.', - 'sharing_management' => 'Gestion des options de partage', - 'bad_opml_file' => 'Votre fichier OPML n’est pas valide.', - 'shortcuts_updated' => 'Les raccourcis ont été mis à jour.', - 'shortcuts_navigation' => 'Navigation', - 'shortcuts_navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.
Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.', - 'shortcuts_article_action' => 'Actions associées à l’article courant', - 'shortcuts_other_action' => 'Autres actions', - 'feeds_marked_read' => 'Les flux ont été marqués comme lus.', - 'updated' => 'Modifications enregistrées.', - - 'already_subscribed' => 'Vous êtes déjà abonné à %s', - 'feed_added' => 'Le flux %s a bien été ajouté.', - 'feed_not_added' => '%s n’a pas pu être ajouté.', - 'internal_problem_feed' => 'Le flux ne peut pas être ajouté. Consulter les logs de FreshRSS pour plus de détails.', - 'invalid_url' => 'L’url %s est invalide.', - 'feed_actualized' => '%s a été mis à jour.', - 'n_feeds_actualized' => '%d flux ont été mis à jour.', - 'feeds_actualized' => 'Les flux ont été mis à jour.', - 'no_feed_actualized' => 'Aucun flux n’a pu être mis à jour.', - 'n_entries_deleted' => '%d articles ont été supprimés.', - 'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues.', - 'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.', - 'category_emptied' => 'La catégorie a été vidée.', - 'feed_deleted' => 'Le flux a été supprimé.', - 'feed_validator' => 'Vérifier la valididé du flux', - - 'optimization_complete' => 'Optimisation terminée.', - - 'your_rss_feeds' => 'Vos flux RSS', - 'your_favorites' => 'Vos favoris', - 'public' => 'Public', - 'invalid_login' => 'L’identifiant est invalide !', - - 'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans.', - - // VIEWS - 'save' => 'Enregistrer', - 'delete' => 'Supprimer', - 'cancel' => 'Annuler', - 'submit' => 'Valider', - - 'back_to_rss_feeds' => '← Retour à vos flux RSS', - 'feeds_moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans %s.', - 'category_number' => 'Catégorie n°%d', - 'ask_empty' => 'Vider ?', - 'number_feeds' => '%d flux', - 'can_not_be_deleted' => 'Ne peut pas être supprimée.', - 'add_category' => 'Ajouter une catégorie', - 'new_category' => 'Nouvelle catégorie', - - 'javascript_for_shortcuts' => 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis.', - 'javascript_should_be_activated'=> 'Le JavaScript doit être activé.', - 'shift_for_all_read' => '+ shift pour marquer tous les articles comme lus', - 'see_on_website' => 'Voir sur le site d’origine', - 'next_article' => 'Passer à l’article suivant', - 'last_article' => 'Passer au dernier article', - 'previous_article' => 'Passer à l’article précédent', - 'first_article' => 'Passer au premier article', - 'next_page' => 'Passer à la page suivante', - 'previous_page' => 'Passer à la page précédente', - 'collapse_article' => 'Refermer', - 'auto_share' => 'Partager', - 'auto_share_help' => 'S’il n’y a qu’un mode de partage, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', - 'focus_search' => 'Accéder à la recherche', - 'user_filter' => 'Accéder aux filtres utilisateur', - 'user_filter_help' => 'S’il n’y a qu’un filtre utilisateur, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', - 'help' => 'Afficher la documentation', - - 'file_to_import' => 'Fichier à importer
(OPML, Json ou Zip)', - 'file_to_import_no_zip' => 'Fichier à importer
(OPML ou Json)', - 'import' => 'Importer', - 'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé!', - '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.', - 'export' => 'Exporter', - 'export_opml' => 'Exporter la liste des flux (OPML)', - 'export_starred' => 'Exporter les favoris', - 'export_no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.', - 'starred_list' => 'Liste des articles favoris', - 'feed_list' => 'Liste des articles de %s', - 'or' => 'ou', - - 'informations' => 'Informations', - 'damn' => 'Arf !', - 'ok' => 'Ok !', - 'attention' => 'Attention !', - 'feed_in_error' => 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.', - 'feed_empty' => 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.', - 'feed_description' => 'Description', - 'website_url' => 'URL du site', - 'feed_url' => 'URL du flux', - 'articles' => 'articles', - 'number_articles' => '%d articles', - 'by_feed' => 'par flux', - 'by_default' => 'Par défaut', - 'keep_history' => 'Nombre minimum d’articles à conserver', - 'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que', - 'categorize' => 'Ranger dans une catégorie', - 'truncate' => 'Supprimer tous les articles', - 'advanced' => 'Avancé', - 'show_in_all_flux' => 'Afficher dans le flux principal', - 'yes' => 'Oui', - 'no' => 'Non', - 'css_path_on_website' => 'Sélecteur CSS des articles sur le site d’origine', - 'retrieve_truncated_feeds' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)', - 'http_authentication' => 'Authentification HTTP', - 'http_username' => 'Identifiant HTTP', - 'http_password' => 'Mot de passe HTTP', - 'blank_to_disable' => 'Laissez vide pour désactiver', - 'share_name' => 'Nom du partage à afficher', - 'share_url' => 'URL du partage à utiliser', - 'not_yet_implemented' => 'Pas encore implémenté', - 'access_protected_feeds' => 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.', - 'no_selected_feed' => 'Aucun flux sélectionné.', - 'think_to_add' => 'Vous pouvez ajouter des flux.', - - 'current_user' => 'Utilisateur actuel', - 'password_form' => 'Mot de passe
(pour connexion par formulaire)', - 'password_api' => 'Mot de passe API
(ex. : pour applis mobiles)', - 'default_user' => 'Nom de l’utilisateur par défaut (16 caractères alphanumériques maximum)', - 'persona_connection_email' => 'Adresse courriel de connexion
(pour Mozilla Persona)', - 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', - 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', - 'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ', - 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applis mobiles)', - 'auth_token' => 'Jeton d’identification', - 'explain_token' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.
%s?output=rss&token=%s', - 'login_configuration' => 'Identification', - 'is_admin' => 'est administrateur', - 'auth_type' => 'Méthode d’authentification', - 'auth_none' => 'Aucune (dangereux)', - 'auth_form' => 'Formulaire (traditionnel, requiert JavaScript)', - 'http_auth' => 'HTTP (pour utilisateurs avancés avec HTTPS)', - 'auth_persona' => 'Mozilla Persona (moderne, requiert JavaScript)', - 'users_list' => 'Liste des utilisateurs', - 'create_user' => 'Créer un nouvel utilisateur', - 'username' => 'Nom d’utilisateur', - 'username_admin' => 'Nom d’utilisateur administrateur', - 'password' => 'Mot de passe', - 'create' => 'Créer', - 'user_created' => 'L’utilisateur %s a été créé.', - 'user_deleted' => 'L’utilisateur %s a été supprimé.', - - 'language' => 'Langue', - 'month' => 'mois', - 'archiving_configuration' => 'Archivage', - 'delete_articles_every' => 'Supprimer les articles après', - 'purge_now' => 'Purger maintenant', - 'purge_completed' => 'Purge effectuée (%d articles supprimés).', - 'archiving_configuration_help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.', - 'reading_configuration' => 'Lecture', - 'display_configuration' => 'Affichage', - 'articles_per_page' => 'Nombre d’articles par page', - 'number_divided_when_reader' => 'Divisé par 2 dans la vue de lecture.', - 'default_view' => 'Vue par défaut', - 'articles_to_display' => 'Articles à afficher', - 'sort_order' => 'Ordre de tri', - 'auto_load_more' => 'Charger les articles suivants en bas de page', - 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', - 'display_categories_unfolded' => 'Afficher les catégories pliées par défaut', - 'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)', - 'after_onread' => 'Après “marquer tout comme lu”,', - 'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)', - 'article_icons' => 'Icônes d’article', - 'top_line' => 'Ligne du haut', - 'bottom_line' => 'Ligne du bas', - 'html5_notif_timeout' => 'Temps d’affichage de la notification HTML5', - 'seconds_(0_means_no_timeout)' => 'secondes (0 signifie aucun timeout ) ', - 'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images', - 'sticky_post' => 'Aligner l’article en haut quand il est ouvert', - 'reading_confirm' => 'Afficher une confirmation lors des actions “marquer tout comme lu”', - 'auto_read_when' => 'Marquer un article comme lu…', - 'article_viewed' => 'lorsque l’article est affiché', - 'article_open_on_website' => 'lorsque l’article est ouvert sur le site d’origine', - 'scroll' => 'au défilement de la page', - 'upon_reception' => 'dès la réception du nouvel article', - 'your_shaarli' => 'Votre Shaarli', - 'your_wallabag' => 'Votre wallabag', - 'your_diaspora_pod' => 'Votre pod Diaspora*', - 'sharing' => 'Partage', - 'share' => 'Partager', - 'by_email' => 'Par courriel', - 'optimize_bdd' => 'Optimiser la base de données', - 'optimize_todo_sometimes' => 'À faire de temps en temps pour réduire la taille de la BDD', - 'theme' => 'Thème', - 'content_width' => 'Largeur du contenu', - 'width_thin' => 'Fine', - 'width_medium' => 'Moyenne', - 'width_large' => 'Large', - 'width_no_limit' => 'Pas de limite', - 'more_information' => 'Plus d’informations', - 'activate_sharing' => 'Activer le partage', - 'shaarli' => 'Shaarli', - 'blogotext' => 'Blogotext', - 'wallabag' => 'wallabag', - 'diaspora' => 'Diaspora*', - 'twitter' => 'Twitter', - 'g+' => 'Google+', - 'facebook' => 'Facebook', - 'email' => 'Courriel', - 'print' => 'Imprimer', - - 'article' => 'Article', - 'title' => 'Titre', - 'author' => 'Auteur', - 'publication_date' => 'Date de publication', - 'by' => 'par', - - 'load_more' => 'Charger plus d’articles', - 'nothing_to_load' => 'Fin des articles', - - 'rss_feeds_of' => 'Flux RSS de %s', - - 'refresh' => 'Actualisation', - 'no_feed_to_refresh' => 'Il n’y a aucun flux à actualiser…', - - 'today' => 'Aujourd’hui', - 'yesterday' => 'Hier', - 'before_yesterday' => 'À partir d’avant-hier', - 'new_article' => 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.', - 'by_author' => 'Par %s', - 'related_tags' => 'Tags associés', - 'no_feed_to_display' => 'Il n’y a aucun article à afficher.', - - 'about_freshrss' => 'À propos de FreshRSS', - 'project_website' => 'Site du projet', - 'lead_developer' => 'Développeur principal', - 'website' => 'Site Internet', - 'bugs_reports' => 'Rapports de bugs', - 'github_or_email' => 'sur Github ou par courriel', - 'license' => 'Licence', - 'agpl3' => 'AGPL 3', - 'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de Kriss Feed ou Leed. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.', - 'credits' => 'Crédits', - 'credits_content' => 'Des éléments de design sont issus du projet Bootstrap bien que FreshRSS n’utilise pas ce framework. Les icônes sont issues du projet GNOME. La police Open Sans utilisée a été créée par Steve Matteson. Les favicons sont récupérés grâce au site getFavicon. FreshRSS repose sur Minz, un framework PHP.', - 'version' => 'Version', - - 'logs' => 'Logs', - 'logs_empty' => 'Les logs sont vides.', - 'clear_logs' => 'Effacer les logs', - - 'forbidden_access' => 'L’accès vous est interdit !', - 'login_required' => 'Accès protégé par mot de passe :', - - 'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !', - 'confirm_action_feed_cat' => 'Êtes-vous sûr(e) de vouloir continuer ? Vous pourriez perdre les favoris et les filtres associés. Cette action ne peut être annulée !', - 'notif_title_new_articles' => 'FreshRSS : nouveaux articles !', - 'notif_body_new_articles' => 'Il y a \d nouveaux articles à lire sur FreshRSS.', - - // DATE - 'january' => 'janvier', - 'february' => 'février', - 'march' => 'mars', - 'april' => 'avril', - 'may' => 'mai', - 'june' => 'juin', - 'july' => 'juillet', - 'august' => 'août', - 'september' => 'septembre', - 'october' => 'octobre', - 'november' => 'novembre', - 'december' => 'décembre', - 'jan' => 'jan.', - 'feb' => 'fév.', - 'mar' => 'mar.', - 'apr' => 'avr.', - 'may' => 'mai.', - 'jun' => 'juin', - 'jul' => 'jui.', - 'aug' => 'août', - 'sep' => 'sep.', - 'oct' => 'oct.', - 'nov' => 'nov.', - 'dec' => 'déc.', - 'sun' => 'dim.', - 'mon' => 'lun.', - 'tue' => 'mar.', - 'wed' => 'mer.', - 'thu' => 'jeu.', - 'fri' => 'ven.', - 'sat' => 'sam.', - // format spécial pour la fonction date() - 'Jan' => '\j\a\n\v\i\e\r', - 'Feb' => '\f\é\v\r\i\e\r', - 'Mar' => '\m\a\r\s', - 'Apr' => '\a\v\r\i\l', - 'May' => '\m\a\i', - 'Jun' => '\j\u\i\n', - 'Jul' => '\j\u\i\l\l\e\t', - 'Aug' => '\a\o\û\t', - 'Sep' => '\s\e\p\t\e\m\b\r\e', - 'Oct' => '\o\c\t\o\b\r\e', - 'Nov' => '\n\o\v\e\m\b\r\e', - 'Dec' => '\d\é\c\e\m\b\r\e', - // format pour la fonction date(), %s permet d'indiquer le mois en toutes lettres - 'format_date' => 'j %s Y', - 'format_date_hour' => 'j %s Y \à H\:i', - - 'status_favorites' => 'favoris', - 'status_read' => 'lus', - 'status_unread' => 'non lus', - 'status_total' => 'total', - - 'stats_entry_repartition' => 'Répartition des articles', - 'stats_entry_per_day' => 'Nombre d’articles par jour (30 derniers jours)', - 'stats_feed_per_category' => 'Flux par catégorie', - 'stats_entry_per_category' => 'Articles par catégorie', - 'stats_top_feed' => 'Les dix plus gros flux', - 'stats_entry_count' => 'Nombre d’articles', - 'stats_no_idle' => 'Il n’y a aucun flux inactif !', - - 'update' => 'Mise à jour', - 'update_system' => 'Système de mise à jour', - 'update_check' => 'Vérifier les mises à jour', - 'update_last' => 'Dernière vérification : %s', - 'update_can_apply' => 'Une mise à jour est disponible.', - 'update_apply' => 'Appliquer la mise à jour', - 'update_server_not_found' => 'Le serveur de mise à jour n’a pas été trouvé. [%s]', - 'no_update' => 'Aucune mise à jour à appliquer', - 'update_problem' => 'La mise à jour a rencontré un problème : %s', - 'update_finished' => 'La mise à jour est terminée !', - - 'auth_reset' => 'Réinitialisation de l’authentification', - 'auth_will_reset' => 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.', - 'auth_not_persona' => 'Seul le système d’authentification Persona peut être réinitialisé.', - 'auth_no_password_set' => 'Aucun mot de passe administrateur n’a été précisé. Cette fonctionnalité n’est pas disponible.', - 'auth_form_set' => 'Le formulaire est désormais votre système d’authentification.', - 'auth_form_not_set' => 'Un problème est survenu lors de la configuration de votre système d’authentification. Veuillez réessayer plus tard.', -); diff --git a/sources/app/i18n/fr/admin.php b/sources/app/i18n/fr/admin.php new file mode 100755 index 0000000..b740bd0 --- /dev/null +++ b/sources/app/i18n/fr/admin.php @@ -0,0 +1,170 @@ + array( + 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', + 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', + 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applis mobiles)', + 'form' => 'Formulaire (traditionnel, requiert JavaScript)', + 'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)', + 'none' => 'Aucune (dangereux)', + 'persona' => 'Mozilla Persona (moderne, requiert JavaScript)', + 'title' => 'Authentification', + 'title_reset' => 'Réinitialisation de l’authentification', + 'token' => 'Jeton d’identification', + 'token_help' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier :', + 'type' => 'Méthode d’authentification', + 'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ', + ), + 'check_install' => array( + 'cache' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/cache. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de cache sont bons.', + ), + 'categories' => array( + 'nok' => 'La table category est mal configurée.', + 'ok' => 'La table category est bien configurée.', + ), + 'connection' => array( + 'nok' => 'La connexion à la base de données est impossible.', + 'ok' => 'La connexion à la base de données est bonne.', + ), + 'ctype' => array( + 'nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).', + 'ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).', + ), + 'curl' => array( + 'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).', + 'ok' => 'Vous disposez de cURL.', + ), + 'data' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de data sont bons.', + ), + 'database' => 'Installation de la base de données', + 'dom' => array( + 'nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml).', + 'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.', + ), + 'entries' => array( + 'nok' => 'La table entry est mal configurée.', + 'ok' => 'La table entry est bien configurée.', + ), + 'favicons' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/favicons. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire des favicons sont bons.', + ), + 'feeds' => array( + 'nok' => 'La table feed est mal configurée.', + 'ok' => 'La table feed est bien configurée.', + ), + 'files' => 'Installation des fichiers', + 'json' => array( + 'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).', + 'ok' => 'Vous disposez de l\'extension JSON.', + ), + 'minz' => array( + 'nok' => 'Vous ne disposez pas de la librairie Minz.', + 'ok' => 'Vous disposez du framework Minz', + ), + 'pcre' => array( + 'nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).', + 'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).', + ), + 'pdo' => array( + 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).', + 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/persona. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons.', + ), + 'php' => array( + '_' => 'Installation de PHP', + 'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.', + 'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.', + ), + 'tables' => array( + 'nok' => 'Il manque une ou plusieurs tables en base de données.', + 'ok' => 'Les tables sont bien présentes en base de données.', + ), + 'title' => 'Vérification de l’installation', + 'tokens' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/tokens. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire des tokens sont bons.', + ), + 'users' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/users. Le serveur HTTP doit être capable d’écrire dedans', + '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.', + ), + ), + 'extensions' => array( + 'disabled' => 'Désactivée', + 'empty_list' => 'Il n’y a aucune extension installée.', + 'enabled' => 'Activée', + 'no_configure_view' => 'Cette extension ne peut pas être configurée.', + 'system' => array( + '_' => 'Extensions système', + 'no_rights' => 'Extension système (vous n’avez aucun droit dessus)', + ), + 'title' => 'Extensions', + 'user' => 'Extensions utilisateur', + ), + 'stats' => array( + '_' => 'Statistiques', + 'all_feeds' => 'Tous les flux', + 'category' => 'Catégorie', + 'entry_count' => 'Nombre d’articles', + 'entry_per_category' => 'Articles par catégorie', + 'entry_per_day' => 'Nombre d’articles par jour (30 derniers jours)', + 'entry_per_day_of_week' => 'Par jour de la semaine (moyenne : %.2f messages)', + 'entry_per_hour' => 'Par heure (moyenne : %.2f messages)', + 'entry_per_month' => 'Par mois (moyenne : %.2f messages)', + 'entry_repartition' => 'Répartition des articles', + 'feed' => 'Flux', + 'feed_per_category' => 'Flux par catégorie', + 'idle' => 'Flux inactifs', + 'main' => 'Statistiques principales', + 'main_stream' => 'Flux principal', + 'menu' => array( + 'idle' => 'Flux inactifs', + 'main' => 'Statistiques principales', + 'repartition' => 'Répartition des articles', + ), + 'no_idle' => 'Il n’y a aucun flux inactif !', + 'number_entries' => '%d articles', + 'percent_of_total' => '%% du total', + 'repartition' => 'Répartition des articles', + 'status_favorites' => 'favoris', + 'status_read' => 'lus', + 'status_total' => 'total', + 'status_unread' => 'non lus', + 'title' => 'Statistiques', + 'top_feed' => 'Les dix plus gros flux', + ), + 'update' => array( + '_' => 'Système de mise à jour', + 'apply' => 'Appliquer la mise à jour', + 'check' => 'Vérifier les mises à jour', + 'current_version' => 'Votre version actuelle de FreshRSS est la %s.', + 'last' => 'Dernière vérification : %s', + 'none' => 'Aucune mise à jour à appliquer', + 'title' => 'Système de mise à jour', + ), + 'user' => array( + 'articles_and_size' => '%s articles (%s)', + 'create' => 'Créer un nouvel utilisateur', + 'email_persona' => 'Adresse courriel de connexion
(pour Mozilla Persona)', + 'language' => 'Langue', + 'password_form' => 'Mot de passe
(pour connexion par formulaire)', + 'password_format' => '7 caractères minimum', + 'title' => 'Gestion des utilisateurs', + 'user_list' => 'Liste des utilisateurs', + 'username' => 'Nom d’utilisateur', + 'users' => 'Utilisateurs', + ), +); diff --git a/sources/app/i18n/fr/conf.php b/sources/app/i18n/fr/conf.php new file mode 100755 index 0000000..d38445b --- /dev/null +++ b/sources/app/i18n/fr/conf.php @@ -0,0 +1,169 @@ + array( + '_' => 'Archivage', + 'advanced' => 'Avancé', + 'delete_after' => 'Supprimer les articles après', + 'help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.', + 'keep_history_by_feed' => 'Nombre minimum d’articles à conserver par flux', + 'optimize' => 'Optimiser la base de données', + 'optimize_help' => 'À faire de temps en temps pour réduire la taille de la BDD', + 'purge_now' => 'Purger maintenant', + 'title' => 'Archivage', + 'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que', + ), + 'display' => array( + '_' => 'Affichage', + 'icon' => array( + 'bottom_line' => 'Ligne du bas', + 'entry' => 'Icônes d’article', + 'publication_date' => 'Date de publication', + 'related_tags' => 'Tags associés', + 'sharing' => 'Partage', + 'top_line' => 'Ligne du haut', + ), + 'language' => 'Langue', + 'notif_html5' => array( + 'seconds' => 'secondes (0 signifie aucun timeout)', + 'timeout' => 'Temps d’affichage de la notification HTML5', + ), + 'theme' => 'Thème', + 'title' => 'Affichage', + 'width' => array( + 'content' => 'Largeur du contenu', + 'large' => 'Large', + 'medium' => 'Moyenne', + 'no_limit' => 'Pas de limite', + 'thin' => 'Fine', + ), + ), + 'query' => array( + '_' => 'Filtres utilisateurs', + 'deprecated' => 'Ce filtre n’est plus valide. La catégorie ou le flux concerné a été supprimé.', + 'filter' => 'Filtres appliqués :', + 'get_all' => 'Afficher tous les articles', + 'get_category' => 'Afficher la catégorie "%s"', + 'get_favorite' => 'Afficher les articles favoris', + 'get_feed' => 'Afficher le flux "%s"', + 'no_filter' => 'Aucun filtre appliqué', + 'none' => 'Vous n’avez pas encore créé de filtre.', + 'number' => 'Filtre n°%d', + 'order_asc' => 'Afficher les articles les plus anciens en premier', + 'order_desc' => 'Afficher les articles les plus récents en premier', + 'search' => 'Recherche de "%s"', + 'state_0' => 'Afficher tous les articles', + 'state_1' => 'Afficher les articles lus', + 'state_2' => 'Afficher les articles non lus', + 'state_3' => 'Afficher tous les articles', + 'state_4' => 'Afficher les articles favoris', + 'state_5' => 'Afficher les articles lus et favoris', + 'state_6' => 'Afficher les articles non lus et favoris', + 'state_7' => 'Afficher les articles favoris', + 'state_8' => 'Afficher les articles non favoris', + 'state_9' => 'Afficher les articles lus et non favoris', + 'state_10' => 'Afficher les articles non lus et non favoris', + 'state_11' => 'Afficher les articles non favoris', + 'state_12' => 'Afficher tous les articles', + 'state_13' => 'Afficher les articles lus', + 'state_14' => 'Afficher les articles non lus', + 'state_15' => 'Afficher tous les articles', + 'title' => 'Filtres utilisateurs', + ), + 'profile' => array( + '_' => 'Gestion du profil', + 'email_persona' => 'Adresse courriel de connexion
(pour Mozilla Persona)', + 'password_api' => 'Mot de passe API
(ex. : pour applis mobiles)', + 'password_form' => 'Mot de passe
(pour connexion par formulaire)', + 'password_format' => '7 caractères minimum', + 'title' => 'Profil', + ), + 'reading' => array( + '_' => 'Lecture', + 'after_onread' => 'Après “marquer tout comme lu”,', + 'articles_per_page' => 'Nombre d’articles par page', + 'auto_load_more' => 'Charger les articles suivants en bas de page', + 'auto_remove_article' => 'Cacher les articles après lecture', + 'confirm_enabled' => 'Afficher une confirmation lors des actions “marquer tout comme lu”', + 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', + 'display_categories_unfolded' => 'Afficher les catégories pliées par défaut', + 'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)', + 'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images', + 'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)', + 'number_divided_when_reader' => 'Divisé par 2 dans la vue de lecture.', + 'read' => array( + 'article_open_on_website' => 'lorsque l’article est ouvert sur le site d’origine', + 'article_viewed' => 'lorsque l’article est affiché', + 'scroll' => 'au défilement de la page', + 'upon_reception' => 'dès la réception du nouvel article', + 'when' => 'Marquer un article comme lu…', + ), + 'show' => array( + '_' => 'Articles à afficher', + 'adaptive' => 'Adapter l’affichage', + 'all_articles' => 'Afficher tous les articles', + 'unread' => 'Afficher les non lus', + ), + 'sort' => array( + '_' => 'Ordre de tri', + 'newer_first' => 'Plus récents en premier', + 'older_first' => 'Plus anciens en premier', + ), + 'sticky_post' => 'Aligner l’article en haut quand il est ouvert', + 'title' => 'Lecture', + 'view' => array( + 'default' => 'Vue par défaut', + 'global' => 'Vue globale', + 'normal' => 'Vue normale', + 'reader' => 'Vue lecture', + ), + ), + 'sharing' => array( + '_' => 'Partage', + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'Courriel', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'more_information' => 'Plus d’informations', + 'print' => 'Print', + 'shaarli' => 'Shaarli', + 'share_name' => 'Nom du partage à afficher', + 'share_url' => 'URL du partage à utiliser', + 'title' => 'Partage', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'shortcut' => array( + '_' => 'Raccourcis', + 'article_action' => 'Actions associées à l’article courant', + 'auto_share' => 'Partager', + 'auto_share_help' => 'S’il n’y a qu’un mode de partage, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', + 'close_dropdown' => 'Fermer les menus', + 'collapse_article' => 'Refermer', + 'first_article' => 'Passer au premier article', + 'focus_search' => 'Accéder à la recherche', + 'help' => 'Afficher la documentation', + 'javascript' => 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis.', + 'last_article' => 'Passer au dernier article', + 'load_more' => 'Charger plus d’articles', + 'mark_read' => 'Marquer comme lu', + 'mark_favorite' => 'Mettre en favori', + 'navigation' => 'Navigation', + 'navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.
Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.', + 'next_article' => 'Passer à l’article suivant', + 'other_action' => 'Autres actions', + 'previous_article' => 'Passer à l’article précédent', + 'see_on_website' => 'Voir sur le site d’origine', + 'shift_for_all_read' => '+ shift pour marquer tous les articles comme lus', + 'title' => 'Raccourcis', + 'user_filter' => 'Accéder aux filtres utilisateur', + 'user_filter_help' => 'S’il n’y a qu’un filtre utilisateur, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', + ), + 'user' => array( + 'articles_and_size' => '%s articles (%s)', + 'current' => 'Utilisateur actuel', + 'is_admin' => 'est administrateur', + 'users' => 'Utilisateurs', + ), +); diff --git a/sources/app/i18n/fr/feedback.php b/sources/app/i18n/fr/feedback.php new file mode 100755 index 0000000..e2364a2 --- /dev/null +++ b/sources/app/i18n/fr/feedback.php @@ -0,0 +1,110 @@ + array( + 'optimization_complete' => 'Optimisation terminée.', + ), + 'access' => array( + 'denied' => 'Vous n’avez pas le droit d’accéder à cette page !', + 'not_found' => 'La page que vous cherchez n’existe pas !', + ), + 'auth' => array( + 'form' => array( + 'not_set' => 'Un problème est survenu lors de la configuration de votre système d’authentification. Veuillez réessayer plus tard.', + 'set' => 'Le formulaire est désormais votre système d’authentification.', + ), + 'login' => array( + 'invalid' => 'L’identifiant est invalide', + 'success' => 'Vous êtes désormais connecté', + ), + 'logout' => array( + 'success' => 'Vous avez été déconnecté', + ), + 'no_password_set' => 'Aucun mot de passe administrateur n’a été précisé. Cette fonctionnalité n’est pas disponible.', + 'not_persona' => 'Seul le système d’authentification Persona peut être réinitialisé.', + ), + 'conf' => array( + 'error' => 'Une erreur est survenue durant la sauvegarde de la configuration', + 'query_created' => 'Le filtre "%s" a bien été créé.', + 'shortcuts_updated' => 'Les raccourcis ont été mis à jour.', + 'updated' => 'La configuration a été mise à jour', + ), + 'extensions' => array( + 'already_enabled' => '%s est déjà activée', + 'disable' => array( + 'ko' => '%s ne peut pas être désactivée. Consulter les logs de FreshRSS pour plus de détails.', + 'ok' => '%s est désormais désactivée', + ), + 'enable' => array( + 'ko' => '%s ne peut pas être activée. Consulter les logs de FreshRSS pour plus de détails.', + 'ok' => '%s est désormais activée', + ), + 'no_access' => 'Vous n’avez aucun accès sur %s', + 'not_enabled' => '%s n’est pas encore activée', + 'not_found' => '%s n’existe pas', + ), + '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.', + '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.', + ), + 'sub' => array( + 'actualize' => 'Actualiser', + 'category' => array( + 'created' => 'La catégorie %s a été créée.', + 'deleted' => 'La catégorie a été supprimée.', + 'emptied' => 'La catégorie a été vidée.', + 'error' => 'La catégorie n’a pas pu être modifiée', + 'name_exists' => 'Une catégorie possède déjà ce nom.', + 'no_id' => 'Vous devez préciser l’id de la catégorie.', + 'no_name' => 'Vous devez préciser un nom pour la catégorie.', + 'not_delete_default' => 'Vous ne pouvez pas supprimer la catégorie par défaut !', + 'not_exist' => 'Cette catégorie n’existe pas !', + 'over_max' => 'Vous avez atteint votre limite de catégories (%d)', + 'updated' => 'La catégorie a été mise à jour.', + ), + 'feed' => array( + 'actualized' => '%s a été mis à jour.', + 'actualizeds' => 'Les flux ont été mis à jour.', + 'added' => 'Le flux %s a bien été ajouté.', + 'already_subscribed' => 'Vous êtes déjà abonné à %s', + 'deleted' => 'Le flux a été supprimé.', + 'error' => 'Une erreur est survenue', + 'internal_problem' => 'Le flux ne peut pas être ajouté. Consulter les logs de FreshRSS pour plus de détails.', + 'invalid_url' => 'L’url %s est invalide.', + 'marked_read' => 'Les flux ont été marqués comme lus.', + 'n_actualized' => '%d flux ont été mis à jour.', + 'n_entries_deleted' => '%d articles ont été supprimés.', + 'no_refresh' => 'Il n’y a aucun flux à actualiser…', + 'not_added' => '%s n’a pas pu être ajouté.', + 'over_max' => 'Vous avez atteint votre limite de flux (%d)', + 'updated' => 'Le flux a été mis à jour', + ), + 'purge_completed' => 'Purge effectuée (%d articles supprimés).', + ), + 'update' => array( + 'can_apply' => 'FreshRSS va maintenant être mis à jour vers la version %s.', + 'error' => 'La mise à jour a rencontré un problème : %s', + 'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans', + 'finished' => 'La mise à jour est terminée !', + 'none' => 'Aucune mise à jour à appliquer', + 'server_not_found' => 'Le serveur de mise à jour n’a pas été trouvé. [%s]', + ), + 'user' => array( + 'created' => array( + '_' => 'L’utilisateur %s a été créé.', + 'error' => 'L’utilisateur %s ne peut pas être créé.', + ), + 'deleted' => array( + '_' => 'L’utilisateur %s a été supprimé.', + 'error' => 'L’utilisateur %s ne peut pas être supprimé.', + ), + ), + 'profile' => array( + 'error' => 'Votre profil n’a pas pu être mis à jour', + 'updated' => 'Votre profil a été mis à jour', + ), +); diff --git a/sources/app/i18n/fr/gen.php b/sources/app/i18n/fr/gen.php new file mode 100755 index 0000000..1cfec69 --- /dev/null +++ b/sources/app/i18n/fr/gen.php @@ -0,0 +1,163 @@ + array( + 'actualize' => 'Actualiser', + 'back_to_rss_feeds' => '← Retour à vos flux RSS', + 'cancel' => 'Annuler', + 'create' => 'Créer', + 'disable' => 'Désactiver', + 'empty' => 'Vider', + 'enable' => 'Activer', + 'export' => 'Exporter', + 'filter' => 'Filtrer', + 'import' => 'Importer', + 'manage' => 'Gérer', + 'mark_read' => 'Marquer comme lu', + 'mark_favorite' => 'Mettre en favori', + 'remove' => 'Supprimer', + 'see_website' => 'Voir le site', + 'submit' => 'Valider', + 'truncate' => 'Supprimer tous les articles', + ), + 'auth' => array( + 'keep_logged_in' => 'Rester connecté (1 mois)', + 'login' => 'Connexion', + 'login_persona' => 'Connexion avec Persona', + 'login_persona_problem' => 'Problème de connexion à Persona ?', + 'logout' => 'Déconnexion', + 'password' => 'Mot de passe', + 'reset' => 'Réinitialisation de l’authentification', + 'username' => 'Nom d’utilisateur', + 'username_admin' => 'Nom d’utilisateur administrateur', + 'will_reset' => 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.', + ), + 'date' => array( + 'Apr' => '\\a\\v\\r\\i\\l', + 'Aug' => '\\a\\o\\û\\t', + 'Dec' => '\\d\\é\\c\\e\\m\\b\\r\\e', + 'Feb' => '\\f\\é\\v\\r\\i\\e\\r', + 'Jan' => '\\j\\a\\n\\v\\i\\e\\r', + 'Jul' => '\\j\\u\\i\\l\\l\\e\\t', + 'Jun' => '\\j\\u\\i\\n', + 'Mar' => '\\m\\a\\r\\s', + 'May' => '\\m\\a\\i', + 'Nov' => '\\n\\o\\v\\e\\m\\b\\r\\e', + 'Oct' => '\\o\\c\\t\\o\\b\\r\\e', + 'Sep' => '\\s\\e\\p\\t\\e\\m\\b\\r\\e', + 'apr' => 'avr.', + 'april' => 'avril', + 'aug' => 'août', + 'august' => 'août', + 'before_yesterday' => 'À partir d’avant-hier', + 'dec' => 'déc.', + 'december' => 'décembre', + 'feb' => 'fév.', + 'february' => 'février', + 'format_date' => 'j %s Y', + 'format_date_hour' => 'j %s Y \\à H\\:i', + 'fri' => 'ven.', + 'jan' => 'jan.', + 'january' => 'janvier', + 'jul' => 'jui.', + 'july' => 'juillet', + 'jun' => 'juin', + 'june' => 'juin', + 'last_3_month' => 'Depuis les trois derniers mois', + 'last_6_month' => 'Depuis les six derniers mois', + 'last_month' => 'Depuis le mois dernier', + 'last_week' => 'Depuis la semaine dernière', + 'last_year' => 'Depuis l’année dernière', + 'mar' => 'mar.', + 'march' => 'mars', + 'may' => 'mai.', + 'mon' => 'lun.', + 'month' => 'mois', + 'nov' => 'nov.', + 'november' => 'novembre', + 'oct' => 'oct.', + 'october' => 'octobre', + 'sat' => 'sam.', + 'sep' => 'sep.', + 'september' => 'septembre', + 'sun' => 'dim.', + 'thu' => 'jeu.', + 'today' => 'Aujourd’hui', + 'tue' => 'mar.', + 'wed' => 'mer.', + 'yesterday' => 'Hier', + ), + 'freshrss' => array( + '_' => 'FreshRSS', + 'about' => 'À propos de FreshRSS', + ), + 'js' => array( + 'category_empty' => 'Catégorie vide', + 'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !', + 'confirm_action_feed_cat' => 'Êtes-vous sûr(e) de vouloir continuer ? Vous perdrez les favoris et les filtres associés. Cette action ne peut être annulée !', + 'feedback' => array( + 'body_new_articles' => 'Il y a \\d nouveaux articles à lire sur FreshRSS.', + 'request_failed' => 'Une requête a échoué, cela peut être dû à des problèmes de connexion à Internet.', + 'title_new_articles' => 'FreshRSS : nouveaux articles !', + ), + 'new_article' => 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.', + 'should_be_activated' => 'Le JavaScript doit être activé.', + ), + 'lang' => array( + 'de' => 'Deutsch', + 'en' => 'English', + 'fr' => 'Français', + ), + 'menu' => array( + 'about' => 'À propos', + 'admin' => 'Administration', + 'archiving' => 'Archivage', + 'authentication' => 'Authentification', + 'check_install' => 'Vérification de l’installation', + 'configuration' => 'Configuration', + 'display' => 'Affichage', + 'extensions' => 'Extensions', + 'logs' => 'Logs', + 'queries' => 'Filtres utilisateurs', + 'reading' => 'Lecture', + 'search' => 'Rechercher des mots ou des #tags', + 'sharing' => 'Partage', + 'shortcuts' => 'Raccourcis', + 'stats' => 'Statistiques', + 'update' => 'Mise à jour', + 'user_management' => 'Gestion des utilisateurs', + 'user_profile' => 'Profil', + ), + 'pagination' => array( + 'first' => 'Début', + 'last' => 'Fin', + 'load_more' => 'Charger plus d’articles', + 'mark_all_read' => 'Tout marquer comme lu', + 'next' => 'Suivant', + 'nothing_to_load' => 'Fin des articles', + 'previous' => 'Précédent', + ), + 'share' => array( + 'blogotext' => 'Blogotext', + 'diaspora' => 'Diaspora*', + 'email' => 'Courriel', + 'facebook' => 'Facebook', + 'g+' => 'Google+', + 'print' => 'Imprimer', + 'shaarli' => 'Shaarli', + 'twitter' => 'Twitter', + 'wallabag' => 'wallabag', + ), + 'short' => array( + 'attention' => 'Attention !', + 'blank_to_disable' => 'Laissez vide pour désactiver', + 'by_author' => 'Par %s', + 'by_default' => 'Par défaut', + 'damn' => 'Arf !', + 'default_category' => 'Sans catégorie', + 'no' => 'Non', + 'ok' => 'Ok !', + 'or' => 'ou', + 'yes' => 'Oui', + ), +); diff --git a/sources/app/i18n/fr/index.php b/sources/app/i18n/fr/index.php new file mode 100755 index 0000000..7e028ab --- /dev/null +++ b/sources/app/i18n/fr/index.php @@ -0,0 +1,61 @@ + array( + '_' => 'À propos', + 'agpl3' => 'AGPL 3', + 'bugs_reports' => 'Rapports de bugs', + 'credits' => 'Crédits', + 'credits_content' => 'Des éléments de design sont issus du projet Bootstrap bien que FreshRSS n’utilise pas ce framework. Les icônes sont issues du projet GNOME. La police Open Sans utilisée a été créée par Steve Matteson. Les favicons sont récupérés grâce au site getFavicon. FreshRSS repose sur Minz, un framework PHP.', + 'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de Kriss Feed ou Leed. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.', + 'github' => 'sur Github', + 'license' => 'Licence', + 'project_website' => 'Site du projet', + 'title' => 'À propos', + 'version' => 'Version', + 'website' => 'Site Internet', + ), + 'feed' => array( + 'add' => 'Vous pouvez ajouter des flux.', + 'empty' => 'Il n’y a aucun article à afficher.', + 'rss_of' => 'Flux RSS de %s', + 'title' => 'Vos flux RSS', + 'title_global' => 'Vue globale', + 'title_fav' => 'Vos favoris', + ), + 'log' => array( + '_' => 'Logs', + 'clear' => 'Effacer les logs', + 'empty' => 'Les logs sont vides.', + 'title' => 'Logs', + ), + 'menu' => array( + 'about' => 'À propos de FreshRSS', + 'add_query' => 'Créer un filtre', + 'before_one_day' => 'Antérieurs à 1 jour', + 'before_one_week' => 'Antérieurs à 1 semaine', + 'favorites' => 'Favoris (%s)', + 'global_view' => 'Vue globale', + 'main_stream' => 'Flux principal', + 'mark_all_read' => 'Tout marquer comme lu', + 'mark_cat_read' => 'Marquer la catégorie comme lue', + 'mark_feed_read' => 'Marquer le flux comme lu', + 'newer_first' => 'Plus récents en premier', + 'non-starred' => 'Afficher tout sauf les favoris', + 'normal_view' => 'Vue normale', + 'older_first' => 'Plus anciens en premier', + 'queries' => 'Filtres utilisateurs', + 'read' => 'Afficher les lus', + 'reader_view' => 'Vue lecture', + 'rss_view' => 'Flux RSS', + 'search_short' => 'Rechercher', + 'starred' => 'Afficher les favoris', + 'stats' => 'Statistiques', + 'subscription' => 'Gestion des abonnements', + 'unread' => 'Afficher les non lus', + ), + 'share' => 'Partager', + 'tag' => array( + 'related' => 'Tags associés', + ), +); diff --git a/sources/app/i18n/fr/install.php b/sources/app/i18n/fr/install.php new file mode 100755 index 0000000..245a20c --- /dev/null +++ b/sources/app/i18n/fr/install.php @@ -0,0 +1,107 @@ + array( + 'finish' => 'Terminer l’installation', + 'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.', + 'next_step' => 'Passer à l’étape suivante', + ), + 'auth' => array( + 'email_persona' => 'Adresse courriel de connexion
(pour Mozilla Persona)', + 'form' => 'Formulaire (traditionnel, requiert JavaScript)', + 'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)', + 'none' => 'Aucune (dangereux)', + 'password_form' => 'Mot de passe
(pour connexion par formulaire)', + 'password_format' => '7 caractères minimum', + 'persona' => 'Mozilla Persona (moderne, requiert JavaScript)', + 'type' => 'Méthode d’authentification', + ), + 'bdd' => array( + '_' => 'Base de données', + 'conf' => array( + '_' => 'Configuration de la base de données', + 'ko' => 'Vérifiez les informations d’accès à la base de données.', + 'ok' => 'La configuration de la base de données a été enregistrée.', + ), + 'host' => 'Hôte', + 'password' => 'Mot de passe', + 'prefix' => 'Préfixe des tables', + 'type' => 'Type de base de données', + 'username' => 'Nom d’utilisateur', + ), + 'check' => array( + '_' => 'Vérifications', + 'cache' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/cache. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de cache sont bons.', + ), + 'ctype' => array( + 'nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).', + 'ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).', + ), + 'curl' => array( + 'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).', + 'ok' => 'Vous disposez de cURL.', + ), + 'data' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de data sont bons.', + ), + 'dom' => array( + 'nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml).', + 'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.', + ), + 'favicons' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/favicons. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire des favicons sont bons.', + ), + 'http_referer' => array( + 'nok' => 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.', + 'ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.', + ), + 'minz' => array( + 'nok' => 'Vous ne disposez pas de la librairie Minz.', + 'ok' => 'Vous disposez du framework Minz', + ), + 'pcre' => array( + 'nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).', + 'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).', + ), + 'pdo' => array( + 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).', + 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).', + ), + 'persona' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/persona. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons.', + ), + 'php' => array( + 'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.', + 'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.', + ), + 'users' => array( + 'nok' => 'Veuillez vérifier les droits sur le répertoire ./data/users. Le serveur HTTP doit être capable d’écrire dedans', + 'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.', + ), + ), + 'conf' => array( + '_' => 'Configuration générale', + 'ok' => 'La configuration générale a été enregistrée.', + ), + 'congratulations' => 'Félicitations !', + 'default_user' => 'Nom de l’utilisateur par défaut (16 caractères alphanumériques maximum)', + 'delete_articles_after' => 'Supprimer les articles après', + 'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.', + 'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé', + 'language' => array( + '_' => 'Langue', + 'choose' => 'Choisissez la langue pour FreshRSS', + 'defined' => 'La langue a bien été définie.', + ), + 'not_deleted' => 'Quelque chose s’est mal passé, vous devez supprimer le fichier %s à la main.', + 'ok' => 'L’installation s’est bien passée.', + 'step' => 'étape %d', + 'steps' => 'Étapes', + 'title' => 'Installation · FreshRSS', + 'this_is_the_end' => 'This is the end', +); diff --git a/sources/app/i18n/fr/sub.php b/sources/app/i18n/fr/sub.php new file mode 100755 index 0000000..a3f7c4d --- /dev/null +++ b/sources/app/i18n/fr/sub.php @@ -0,0 +1,61 @@ + array( + '_' => 'Catégorie', + 'add' => 'Ajouter une catégorie', + 'empty' => 'Catégorie vide', + 'new' => 'Nouvelle catégorie', + ), + 'feed' => array( + 'add' => 'Ajouter un flux RSS', + 'advanced' => 'Avancé', + 'archiving' => 'Archivage', + 'auth' => array( + 'configuration' => 'Identification', + 'help' => 'La connexion permet d’accéder aux flux protégés par une authentification HTTP.', + 'http' => 'Authentification HTTP', + 'password' => 'Mot de passe HTTP', + 'username' => 'Identifiant HTTP', + ), + 'css_help' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)', + 'css_path' => 'Sélecteur CSS des articles sur le site d’origine', + 'description' => 'Description', + 'empty' => 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.', + 'error' => 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.', + 'in_main_stream' => 'Afficher dans le flux principal', + 'informations' => 'Informations', + 'keep_history' => 'Nombre minimum d’articles à conserver', + 'moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans %s.', + 'no_selected' => 'Aucun flux sélectionné.', + 'number_entries' => '%d articles', + 'stats' => 'Statistiques', + 'think_to_add' => 'Vous pouvez ajouter des flux.', + 'title' => 'Titre', + 'title_add' => 'Ajouter un flux RSS', + 'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que', + 'url' => 'URL du flux', + 'validator' => 'Vérifier la valididé du flux', + 'website' => 'URL du site', + ), + 'import_export' => array( + 'export' => 'Exporter', + '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
(OPML, Json ou Zip)', + 'file_to_import_no_zip' => 'Fichier à importer
(OPML ou Json)', + 'import' => 'Importer', + 'starred_list' => 'Liste des articles favoris', + 'title' => 'Importer / exporter', + ), + 'menu' => array( + 'bookmark' => 'S’abonner (bookmark FreshRSS)', + 'import_export' => 'Importer / exporter', + 'subscription_management' => 'Gestion des abonnements', + ), + 'title' => array( + '_' => 'Gestion des abonnements', + 'feed_management' => 'Gestion des flux RSS', + ), +); diff --git a/sources/app/i18n/install.en.php b/sources/app/i18n/install.en.php deleted file mode 100755 index c422de9..0000000 --- a/sources/app/i18n/install.en.php +++ /dev/null @@ -1,69 +0,0 @@ - 'Installation · FreshRSS', - 'freshrss' => 'FreshRSS', - 'installation_step' => 'Installation — step %d · FreshRSS', - 'steps' => 'Steps', - 'checks' => 'Checks', - 'general_configuration' => 'General configuration', - 'bdd_configuration' => 'Database configuration', - 'bdd_type' => 'Type of database', - 'version_update' => 'Update', - 'this_is_the_end' => 'This is the end', - - 'ok' => 'Ok!', - 'congratulations' => 'Congratulations!', - 'attention' => 'Attention!', - 'damn' => 'Damn!', - 'oops' => 'Oops!', - 'next_step' => 'Go to the next step', - - 'language_defined' => 'Language has been defined.', - 'choose_language' => 'Choose a language for FreshRSS', - - 'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled', - 'php_is_ok' => 'Your PHP version is %s, which is compatible with FreshRSS', - 'php_is_nok' => 'Your PHP version is %s but FreshRSS requires at least version %s', - 'minz_is_ok' => 'You have the Minz framework', - 'minz_is_nok' => 'You lack the Minz framework. You should execute build.sh script or download it on Github and install in %s directory the content of its /lib directory.', - 'curl_is_ok' => 'You have version %s of cURL', - 'curl_is_nok' => 'You lack cURL (php5-curl package)', - 'pdo_is_ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite)', - 'pdo_is_nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite)', - 'dom_is_ok' => 'You have the required library to browse the DOM', - 'dom_is_nok' => 'You lack a required library to browse the DOM (php-xml package)', - 'pcre_is_ok' => 'You have the required library for regular expressions (PCRE)', - 'pcre_is_nok' => 'You lack a required library for regular expressions (php-pcre)', - 'ctype_is_ok' => 'You have the required library for character type checking (ctype)', - 'ctype_is_nok' => 'You lack a required library for character type checking (php-ctype)', - 'cache_is_ok' => 'Permissions on cache directory are good', - 'log_is_ok' => 'Permissions on logs directory are good', - 'favicons_is_ok' => 'Permissions on favicons directory are good', - 'data_is_ok' => 'Permissions on data directory are good', - 'persona_is_ok' => 'Permissions on Mozilla Persona directory are good', - 'file_is_nok' => 'Check permissions on %s directory. HTTP server must have rights to write into', - 'http_referer_is_ok' => 'Your HTTP REFERER is known and corresponds to your server.', - 'http_referer_is_nok' => 'Please check that you are not altering your HTTP REFERER.', - 'fix_errors_before' => 'Fix errors before skip to the next step.', - - 'general_conf_is_ok' => 'General configuration has been saved.', - 'random_string' => 'Random string', - 'change_value' => 'You should change this value by any other', - 'base_url' => 'Base URL', - 'do_not_change_if_doubt' => 'Don’t change if you doubt about it', - - 'bdd_conf_is_ok' => 'Database configuration has been saved.', - 'bdd_conf_is_ko' => 'Verify your database information.', - 'host' => 'Host', - 'bdd' => 'Database', - 'prefix' => 'Table prefix', - - 'update_start' => 'Start update process', - 'update_long' => 'This can take a long time, depending on the size of your database. You may have to wait for this page to time out (~5 minutes) and then refresh this page.', - 'update_end' => 'Update process is completed, now you can go to the final step.', - - - 'installation_is_ok' => 'The installation process was successful.
The final step will now attempt to delete any file and database backup created during the update process.
You may choose to skip this step by deleting ./data/do-install.txt manually.', - 'finish_installation' => 'Complete installation', - 'install_not_deleted' => 'Something went wrong; you must delete the file %s manually.', -); diff --git a/sources/app/i18n/install.fr.php b/sources/app/i18n/install.fr.php deleted file mode 100755 index 785c024..0000000 --- a/sources/app/i18n/install.fr.php +++ /dev/null @@ -1,68 +0,0 @@ - 'Installation · FreshRSS', - 'freshrss' => 'FreshRSS', - 'installation_step' => 'Installation — étape %d · FreshRSS', - 'steps' => 'Étapes', - 'checks' => 'Vérifications', - 'general_configuration' => 'Configuration générale', - 'bdd_configuration' => 'Base de données', - 'bdd_type' => 'Type de base de données', - 'version_update' => 'Mise à jour', - 'this_is_the_end' => 'This is the end', - - 'ok' => 'Ok !', - 'congratulations' => 'Félicitations !', - 'attention' => 'Attention !', - 'damn' => 'Arf !', - 'oops' => 'Oups !', - 'next_step' => 'Passer à l’étape suivante', - - 'language_defined' => 'La langue a bien été définie.', - 'choose_language' => 'Choisissez la langue pour FreshRSS', - - 'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé', - 'php_is_ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS', - 'php_is_nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s', - 'minz_is_ok' => 'Vous disposez du framework Minz', - 'minz_is_nok' => 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script build.sh ou bien la télécharger sur Github et installer dans le répertoire %s le contenu de son répertoire /lib.', - 'curl_is_ok' => 'Vous disposez de cURL dans sa version %s', - 'curl_is_nok' => 'Vous ne disposez pas de cURL (paquet php5-curl)', - 'pdo_is_ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite)', - 'pdo_is_nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite)', - 'dom_is_ok' => 'Vous disposez du nécessaire pour parcourir le DOM', - 'dom_is_nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml)', - 'pcre_is_ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE)', - 'pcre_is_nok' => 'Il manque une librairie pour les expressions régulières (php-pcre)', - 'ctype_is_ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype)', - 'ctype_is_nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype)', - 'cache_is_ok' => 'Les droits sur le répertoire de cache sont bons', - 'log_is_ok' => 'Les droits sur le répertoire des logs sont bons', - 'favicons_is_ok' => 'Les droits sur le répertoire des favicons sont bons', - 'data_is_ok' => 'Les droits sur le répertoire de data sont bons', - 'persona_is_ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons', - 'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans', - 'http_referer_is_ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.', - 'http_referer_is_nok' => 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.', - 'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.', - - 'general_conf_is_ok' => 'La configuration générale a été enregistrée.', - 'random_string' => 'Chaîne aléatoire', - 'change_value' => 'Vous devriez changer cette valeur par n’importe quelle autre', - 'base_url' => 'Base de l’URL', - 'do_not_change_if_doubt' => 'Laissez tel quel dans le doute', - - 'bdd_conf_is_ok' => 'La configuration de la base de données a été enregistrée.', - 'bdd_conf_is_ko' => 'Vérifiez les informations d’accès à la base de données.', - 'host' => 'Hôte', - 'bdd' => 'Base de données', - 'prefix' => 'Préfixe des tables', - - 'update_start' => 'Lancer la mise à jour', - 'update_long' => 'Ce processus peut prendre longtemps, selon la taille de votre base de données. Vous aurez peut-être à attendre que cette page dépasse son temps maximum d’exécution (~5 minutes) puis à la recharger.', - 'update_end' => 'La mise à jour est terminée, vous pouvez maintenant passer à l’étape finale.', - - 'installation_is_ok' => 'L’installation s’est bien passée.
La dernière étape va maintenant tenter de supprimer les fichiers ainsi que d’éventuelles copies de base de données créés durant le processus de mise à jour.
Vous pouvez choisir de sauter cette étape en supprimant ./data/do-install.txt manuellement.', - 'finish_installation' => 'Terminer l’installation', - 'install_not_deleted' => 'Quelque chose s’est mal passé, vous devez supprimer le fichier %s à la main.', -); diff --git a/sources/app/install.php b/sources/app/install.php index 4449cd0..177173f 100755 --- a/sources/app/install.php +++ b/sources/app/install.php @@ -42,55 +42,24 @@ function param($key, $default = false) { // gestion internationalisation -$translates = array(); -$actual = 'en'; function initTranslate() { - global $translates; - global $actual; + Minz_Translate::init(); + $available_languages = Minz_Translate::availableLanguages(); - $actual = isset($_SESSION['language']) ? $_SESSION['language'] : getBetterLanguage('en'); - - $file = APP_PATH . '/i18n/' . $actual . '.php'; - if (file_exists($file)) { - $translates = array_merge($translates, include($file)); + if (!isset($_SESSION['language'])) { + $_SESSION['language'] = get_best_language(); } - $file = APP_PATH . '/i18n/install.' . $actual . '.php'; - if (file_exists($file)) { - $translates = array_merge($translates, include($file)); + if (!in_array($_SESSION['language'], $available_languages)) { + $_SESSION['language'] = 'en'; } + + Minz_Translate::reset($_SESSION['language']); } -function getBetterLanguage($fallback) { - $available = availableLanguages(); +function get_best_language() { $accept = $_SERVER['HTTP_ACCEPT_LANGUAGE']; - $language = strtolower(substr($accept, 0, 2)); - - if (isset($available[$language])) { - return $language; - } else { - return $fallback; - } -} - -function availableLanguages() { - return array( - 'en' => 'English', - 'fr' => 'Français' - ); -} - -function _t($key) { - global $translates; - $translate = $key; - if (isset($translates[$key])) { - $translate = $translates[$key]; - } - - $args = func_get_args(); - unset($args[0]); - - return vsprintf($translate, $args); + return strtolower(substr($accept, 0, 2)); } @@ -109,7 +78,7 @@ function saveLanguage() { function saveStep2() { if (!empty($_POST)) { - $_SESSION['title'] = substr(trim(param('title', _t('freshrss'))), 0, 25); + $_SESSION['title'] = substr(trim(param('title', _t('gen.freshrss'))), 0, 25); $_SESSION['old_entries'] = param('old_entries', 3); $_SESSION['auth_type'] = param('auth_type', 'form'); $_SESSION['default_user'] = substr(preg_replace('/[^a-zA-Z0-9]/', '', param('default_user', '')), 0, 16); @@ -156,12 +125,17 @@ function saveStep2() { 'token' => $token, ); - $configPath = DATA_PATH . '/' . $_SESSION['default_user'] . '_user.php'; - @unlink($configPath); //To avoid access-rights problems - file_put_contents($configPath, " array( - 'environment' => empty($_SESSION['environment']) ? 'production' : $_SESSION['environment'], - 'salt' => $_SESSION['salt'], - 'base_url' => '', - 'title' => $_SESSION['title'], - 'default_user' => $_SESSION['default_user'], - 'allow_anonymous' => isset($_SESSION['allow_anonymous']) ? $_SESSION['allow_anonymous'] : false, - 'allow_anonymous_refresh' => isset($_SESSION['allow_anonymous_refresh']) ? $_SESSION['allow_anonymous_refresh'] : false, - 'auth_type' => $_SESSION['auth_type'], - 'api_enabled' => isset($_SESSION['api_enabled']) ? $_SESSION['api_enabled'] : false, - 'unsafe_autologin_enabled' => isset($_SESSION['unsafe_autologin_enabled']) ? $_SESSION['unsafe_autologin_enabled'] : false, - ), + $config_array = array( + 'environment' => 'production', + 'salt' => $_SESSION['salt'], + 'title' => $_SESSION['title'], + 'default_user' => $_SESSION['default_user'], + 'auth_type' => $_SESSION['auth_type'], 'db' => array( 'type' => $_SESSION['bd_type'], 'host' => $_SESSION['bd_host'], @@ -217,8 +184,8 @@ function saveStep3() { ), ); - @unlink(DATA_PATH . '/config.php'); //To avoid access-rights problems - file_put_contents(DATA_PATH . '/config.php', " PDO::ERRMODE_EXCEPTION, ); @@ -253,7 +220,7 @@ function newPdo() { } function deleteInstall() { - $res = unlink(DATA_PATH . '/do-install.txt'); + $res = unlink(join_path(DATA_PATH, 'do-install.txt')); if (!$res) { return false; @@ -282,9 +249,9 @@ function checkStep() { } function checkStep0() { - $languages = availableLanguages(); + $languages = Minz_Translate::availableLanguages(); $language = isset($_SESSION['language']) && - isset($languages[$_SESSION['language']]); + in_array($_SESSION['language'], $languages); return array( 'language' => $language ? 'ok' : 'ko', @@ -294,7 +261,7 @@ function checkStep0() { function checkStep1() { $php = version_compare(PHP_VERSION, '5.2.1') >= 0; - $minz = file_exists(LIB_PATH . '/Minz'); + $minz = file_exists(join_path(LIB_PATH, 'Minz')); $curl = extension_loaded('curl'); $pdo_mysql = extension_loaded('pdo_mysql'); $pdo_sqlite = extension_loaded('pdo_sqlite'); @@ -304,9 +271,9 @@ function checkStep1() { $dom = class_exists('DOMDocument'); $data = DATA_PATH && is_writable(DATA_PATH); $cache = CACHE_PATH && is_writable(CACHE_PATH); - $log = LOG_PATH && is_writable(LOG_PATH); - $favicons = is_writable(DATA_PATH . '/favicons'); - $persona = is_writable(DATA_PATH . '/persona'); + $users = USERS_PATH && is_writable(USERS_PATH); + $favicons = is_writable(join_path(DATA_PATH, 'favicons')); + $persona = is_writable(join_path(DATA_PATH, 'persona')); $http_referer = is_referer_from_same_domain(); return array( @@ -321,12 +288,12 @@ function checkStep1() { 'dom' => $dom ? 'ok' : 'ko', 'data' => $data ? 'ok' : 'ko', 'cache' => $cache ? 'ok' : 'ko', - 'log' => $log ? 'ok' : 'ko', + 'users' => $users ? 'ok' : 'ko', 'favicons' => $favicons ? 'ok' : 'ko', 'persona' => $persona ? 'ok' : 'ko', 'http_referer' => $http_referer ? 'ok' : 'ko', 'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && - $data && $cache && $log && $favicons && $persona && $http_referer ? + $data && $cache && $users && $favicons && $persona && $http_referer ? 'ok' : 'ko' ); } @@ -351,7 +318,7 @@ function checkStep2() { if ($defaultUser === null) { $defaultUser = empty($_SESSION['default_user']) ? '' : $_SESSION['default_user']; } - $data = is_writable(DATA_PATH . '/' . $defaultUser . '_user.php'); + $data = is_writable(join_path(USERS_PATH, $defaultUser, 'config.php')); return array( 'conf' => $conf ? 'ok' : 'ko', @@ -363,7 +330,7 @@ function checkStep2() { } function checkStep3() { - $conf = is_writable(DATA_PATH . '/config.php'); + $conf = is_writable(join_path(DATA_PATH, 'config.php')); $bd = isset($_SESSION['bd_type']) && isset($_SESSION['bd_host']) && @@ -406,7 +373,7 @@ function checkBD() { $str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base']; break; case 'sqlite': - $str = 'sqlite:' . DATA_PATH . '/' . $_SESSION['default_user'] . '.sqlite'; + $str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite'); $driver_options = array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ); @@ -418,7 +385,7 @@ function checkBD() { $c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options); if (defined('SQL_CREATE_TABLES')) { - $sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user'], _t('default_category')); + $sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user'], _t('gen.short.default_category')); $stm = $c->prepare($sql); $ok = $stm->execute(); } else { @@ -426,7 +393,7 @@ function checkBD() { if (is_array($SQL_CREATE_TABLES)) { $ok = true; foreach ($SQL_CREATE_TABLES as $instruction) { - $sql = sprintf($instruction, $_SESSION['bd_prefix_user'], _t('default_category')); + $sql = sprintf($instruction, $_SESSION['bd_prefix_user'], _t('gen.short.default_category')); $stm = $c->prepare($sql); $ok &= $stm->execute(); } @@ -438,7 +405,7 @@ function checkBD() { } if (!$ok) { - @unlink(DATA_PATH . '/config.php'); + @unlink(join_path(DATA_PATH, 'config.php')); } return $ok; @@ -446,21 +413,23 @@ function checkBD() { /*** AFFICHAGE ***/ function printStep0() { - global $actual; + $actual = Minz_Translate::language(); + $languages = Minz_Translate::availableLanguages(); ?> -

+

- +
- +
@@ -468,10 +437,10 @@ function printStep0() {
- - + + - +
@@ -479,94 +448,95 @@ function printStep0() { - + -

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

-

+

- -

+ +

-

+

-

+

-

+

-

+

-

+

-

+

-

+

- + -

+

-

+

-

+

- +
- +
- +
- +
- +
- +
- +
- +
- +
/>
- + +
- +
/> - +
diff --git a/sources/app/layout/aside_flux.phtml b/sources/app/layout/aside_flux.phtml deleted file mode 100755 index aac3c08..0000000 --- a/sources/app/layout/aside_flux.phtml +++ /dev/null @@ -1,103 +0,0 @@ -
- - -
    - loginOk) { ?> - - -
  • -
    - - -
    -
  • - -
  • - - - 'index', 'a' => 'index', 'params' => array()); - if ($this->conf->view_mode !== Minz_Request::param('output', 'normal')) { - $arUrl['params']['output'] = 'normal'; - } - ?> -
  • - -
  • - -
  • - -
  • - - cat_aside as $cat) { - $feeds = $cat->feeds(); - if (!empty($feeds)) { - $c_active = false; - $c_show = false; - if ($this->get_c == $cat->id()) { - $c_active = true; - if (!$this->conf->display_categories || $this->get_f) { - $c_show = true; - } - } - ?>
  • >
      id(); - $nbEntries = $feed->nbEntries(); - $f_active = ($this->get_f == $feed_id); - ?>
    • ✇ name(); ?>
  • -
- -
- - diff --git a/sources/app/layout/aside_stats.phtml b/sources/app/layout/aside_stats.phtml index fbfb9d8..4bdaf71 100755 --- a/sources/app/layout/aside_stats.phtml +++ b/sources/app/layout/aside_stats.phtml @@ -1,12 +1,12 @@ diff --git a/sources/app/layout/aside_subscription.phtml b/sources/app/layout/aside_subscription.phtml new file mode 100755 index 0000000..8a54e2d --- /dev/null +++ b/sources/app/layout/aside_subscription.phtml @@ -0,0 +1,17 @@ + diff --git a/sources/app/layout/header.phtml b/sources/app/layout/header.phtml index 028e63d..41a63a5 100755 --- a/sources/app/layout/header.phtml +++ b/sources/app/layout/header.phtml @@ -1,22 +1,12 @@ @@ -26,17 +16,16 @@ if (Minz_Configuration::canLogIn()) {

- + title; ?>

diff --git a/sources/app/layout/layout.phtml b/sources/app/layout/layout.phtml index f95f45b..083ffd4 100755 --- a/sources/app/layout/layout.phtml +++ b/sources/app/layout/layout.phtml @@ -1,5 +1,5 @@ - + @@ -10,21 +10,22 @@ renderHelper('javascript_vars'); ?> //]]> nextId)) { - $params = Minz_Request::params(); - $params['next'] = $this->nextId; - $params['ajax'] = 1; + $url_base = Minz_Request::currentRequest(); + if (FreshRSS_Context::$next_id !== '') { + $url_next = $url_base; + $url_next['params']['next'] = FreshRSS_Context::$next_id; + $url_next['params']['ajax'] = 1; ?> - + url)) { - $rss_url = $this->url; - $rss_url['params']['output'] = 'rss'; + if (isset($this->rss_title)) { + $url_rss = $url_base; + $url_rss['a'] = 'rss'; ?> - + @@ -33,7 +34,7 @@ - + @@ -56,7 +57,7 @@ ?>
- +
diff --git a/sources/app/layout/nav_entries.phtml b/sources/app/layout/nav_entries.phtml index 3141e92..ca68491 100755 --- a/sources/app/layout/nav_entries.phtml +++ b/sources/app/layout/nav_entries.phtml @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/sources/app/layout/nav_menu.phtml b/sources/app/layout/nav_menu.phtml index a9e6614..3a755b5 100755 --- a/sources/app/layout/nav_menu.phtml +++ b/sources/app/layout/nav_menu.phtml @@ -1,90 +1,31 @@ - + + diff --git a/sources/app/views/auth/formLogin.phtml b/sources/app/views/auth/formLogin.phtml new file mode 100755 index 0000000..979e173 --- /dev/null +++ b/sources/app/views/auth/formLogin.phtml @@ -0,0 +1,28 @@ +
+

+ + +
+ + +
+
+ + +
+ +
+
+ +
+
+
+ +
+ + +

+
diff --git a/sources/app/views/auth/index.phtml b/sources/app/views/auth/index.phtml new file mode 100755 index 0000000..f7a862a --- /dev/null +++ b/sources/app/views/auth/index.phtml @@ -0,0 +1,85 @@ +partial('aside_configure'); ?> + +
+ + +
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+ + token; ?> +
+ /> + + array('output' => 'rss', 'token' => $token)), 'html', true); ?> +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+
+
diff --git a/sources/p/i/.htaccess b/sources/app/views/auth/logout.phtml old mode 100644 new mode 100755 similarity index 100% rename from sources/p/i/.htaccess rename to sources/app/views/auth/logout.phtml diff --git a/sources/app/views/auth/personaLogin.phtml b/sources/app/views/auth/personaLogin.phtml new file mode 100755 index 0000000..545ed2e --- /dev/null +++ b/sources/app/views/auth/personaLogin.phtml @@ -0,0 +1,24 @@ +res === false) { ?> +
+

+ +

+ + +

+ + + + + +

+ +

+
+res); +} +?> diff --git a/sources/app/views/index/resetAuth.phtml b/sources/app/views/auth/reset.phtml similarity index 59% rename from sources/app/views/index/resetAuth.phtml rename to sources/app/views/auth/reset.phtml index 6d4282c..6e9816a 100755 --- a/sources/app/views/index/resetAuth.phtml +++ b/sources/app/views/auth/reset.phtml @@ -1,5 +1,5 @@
-

+

message)) { ?>

@@ -9,24 +9,24 @@ no_form) { ?> -

+

-
- +
+

- +
- +
- +
- +
diff --git a/sources/app/views/configure/archiving.phtml b/sources/app/views/configure/archiving.phtml index c9cc7fe..8754631 100755 --- a/sources/app/views/configure/archiving.phtml +++ b/sources/app/views/configure/archiving.phtml @@ -1,31 +1,31 @@ partial('aside_configure'); ?>
- +
- -

+ +

- +
- -   + +  
- +
() + ?> ()
- +
() + ?> ()
- - + +
- +
-

+
-

nb_total), ' ', Minz_Translate::t('articles'), ', ', formatBytes($this->size_user); ?>

- - - + nb_total), format_bytes($this->size_user)); ?>
- +
-

+
-

size_total); ?>

+ size_total); ?> +
+
+ +
+
+ + +
diff --git a/sources/app/views/configure/categorize.phtml b/sources/app/views/configure/categorize.phtml deleted file mode 100755 index 23d1c9f..0000000 --- a/sources/app/views/configure/categorize.phtml +++ /dev/null @@ -1,55 +0,0 @@ -partial ('aside_feed'); ?> - -
- - - - - -

defaultCategory->name ()); ?>

- - categories as $cat) { $i++; ?> -
- -
-
- - - nbFeed () > 0) { ?> - - - - - -
- (nbFeed ()); ?>) - - id () === $this->defaultCategory->id ()) { ?> - - - - -
-
- - -
- -
- -
-
- -
-
- - -
-
- -
diff --git a/sources/app/views/configure/display.phtml b/sources/app/views/configure/display.phtml index 8eb3a15..02249bc 100755 --- a/sources/app/views/configure/display.phtml +++ b/sources/app/views/configure/display.phtml @@ -1,108 +1,122 @@ -partial ('aside_configure'); ?> +partial('aside_configure'); ?>
- + -
- + +
- +
- +
- +
    + themes); $i = 1; ?> + themes as $theme) { ?> + theme === $theme['id']) {echo "checked";}?> value=""/> +
  • +
    + +
    + +
    +
    +
    +
    +
    +
  • + + +
- conf->content_width; ?> + content_width; ?>
- +
- + - - - - - - + + + + + + - - - + + + - - + + - - - - - - - + + + + + + +
 
conf->topline_read ? ' checked="checked"' : ''; ?> />conf->topline_favorite ? ' checked="checked"' : ''; ?> />topline_read ? ' checked="checked"' : ''; ?> />topline_favorite ? ' checked="checked"' : ''; ?> /> conf->topline_date ? ' checked="checked"' : ''; ?> />conf->topline_link ? ' checked="checked"' : ''; ?> />topline_date ? ' checked="checked"' : ''; ?> />topline_link ? ' checked="checked"' : ''; ?> />
conf->bottomline_read ? ' checked="checked"' : ''; ?> />conf->bottomline_favorite ? ' checked="checked"' : ''; ?> />conf->bottomline_sharing ? ' checked="checked"' : ''; ?> />conf->bottomline_tags ? ' checked="checked"' : ''; ?> />conf->bottomline_date ? ' checked="checked"' : ''; ?> />conf->bottomline_link ? ' checked="checked"' : ''; ?> />bottomline_read ? ' checked="checked"' : ''; ?> />bottomline_favorite ? ' checked="checked"' : ''; ?> />bottomline_sharing ? ' checked="checked"' : ''; ?> />bottomline_tags ? ' checked="checked"' : ''; ?> />bottomline_date ? ' checked="checked"' : ''; ?> />bottomline_link ? ' checked="checked"' : ''; ?> />

- +
- +
- - + +
diff --git a/sources/app/views/configure/feed.phtml b/sources/app/views/configure/feed.phtml deleted file mode 100755 index e96a287..0000000 --- a/sources/app/views/configure/feed.phtml +++ /dev/null @@ -1,182 +0,0 @@ -partial ('aside_feed'); ?> - -flux) { ?> -
- - -

flux->name (); ?>

- flux->description (); ?> - - flux->nbEntries (); ?> - - flux->inError ()) { ?> -

- -

- - -
- -
- -
- -
-
-
- -
- -
-
-
- -
-
- - -
-
-
-
- -
-
- - -
- - -
-
-
- -
- -
-
-
- -
- -
-
-
-
- - - -
-
-
-
- - -
-
- - - -
-
-
- - - - -
-
-
-
- -
- -
-
-
- -
- -
-
-
-
- - -
-
- - - flux->httpAuth (false); ?> -
- -
- - -
- - -
- -
-
- -
-
- - -
-
- - -
- -
- - -
-
- -
-
- - -
-
-
-
- - -
- diff --git a/sources/app/views/configure/queries.phtml b/sources/app/views/configure/queries.phtml index e778ce0..5f449de 100755 --- a/sources/app/views/configure/queries.phtml +++ b/sources/app/views/configure/queries.phtml @@ -1,15 +1,15 @@ partial('aside_configure'); ?>
- +
- + - conf->queries as $key => $query) { ?> + queries as $key => $query) { ?>
@@ -49,31 +49,31 @@
-
+
-
+
-
+
    -
  • +
  • -
  • +
  • -
  • +
  • -
  • query_get[$key]['type'], $this->query_get[$key]['name']); ?>
  • +
  • query_get[$key]['type'], $this->query_get[$key]['name']); ?>
@@ -82,15 +82,15 @@
- conf->queries) > 0) { ?> + queries) > 0) { ?>
- - + +
-

+

diff --git a/sources/app/views/configure/reading.phtml b/sources/app/views/configure/reading.phtml index 8b2da2a..636671f 100755 --- a/sources/app/views/configure/reading.phtml +++ b/sources/app/views/configure/reading.phtml @@ -1,47 +1,47 @@ -partial ('aside_configure'); ?> +partial('aside_configure'); ?>
- + -
- + +
- +
- - + +
- +
- +
- +
@@ -49,8 +49,8 @@
@@ -58,9 +58,9 @@
@@ -68,9 +68,9 @@
@@ -78,9 +78,9 @@
@@ -88,9 +88,9 @@
@@ -98,9 +98,9 @@
@@ -108,49 +108,59 @@
- +
+ +
+
+ +
+
- +
- - + +
diff --git a/sources/app/views/configure/sharing.phtml b/sources/app/views/configure/sharing.phtml index 02ce331..da75574 100755 --- a/sources/app/views/configure/sharing.phtml +++ b/sources/app/views/configure/sharing.phtml @@ -1,58 +1,63 @@ -partial ('aside_configure'); ?> +partial('aside_configure'); ?>
- + - +
' data-advanced='
- - -
- + + +
+
'> - - conf->sharing as $key => $sharing): ?> - conf->shares[$sharing['type']]; ?> -
+ + sharing as $key => $share_options) { + $share = FreshRSS_Share::get($share_options['type']); + $share->update($share_options); + ?> +
- ' /> - + + formType() === 'advanced') { ?>
- - - + + +
- + - +
- +
- +
- - + +
diff --git a/sources/app/views/configure/shortcut.phtml b/sources/app/views/configure/shortcut.phtml index a4029b6..f68091a 100755 --- a/sources/app/views/configure/shortcut.phtml +++ b/sources/app/views/configure/shortcut.phtml @@ -1,7 +1,7 @@ -partial ('aside_configure'); ?> +partial('aside_configure'); ?>
- + list_keys as $key) { ?> @@ -9,110 +9,117 @@ - conf->shortcuts; ?> + shortcuts; ?> -
- + + - + - + + +

- +
- +
- +
- +
-
- - +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- + +
+ +
+
+ +
+
@@ -120,8 +127,8 @@
- - + +
diff --git a/sources/app/views/configure/users.phtml b/sources/app/views/configure/users.phtml deleted file mode 100755 index 272896f..0000000 --- a/sources/app/views/configure/users.phtml +++ /dev/null @@ -1,211 +0,0 @@ -partial('aside_configure'); ?> - -
- - -
- - -
- -
- - -
-
- -
- -
-
- /> - -
- -
-
- - -
- -
-
- /> - -
-
-
- - -
- - conf->mail_login; ?> -
- placeholder="alice@example.net" /> - -
-
- -
-
- - -
-
- - - - - -
- -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - -
- - conf->token; ?> -
- /> - -
-
- - -
-
- -
-
- -
-
- - -
-
-
- -
- - -
- -
- -
-
- -
-
- -
-
-
- -
- - -
- -
- -
-
- -
- -
- -
-
- -
- -
-
- - -
- -
-
- -
- - conf->mail_login; ?> -
- -
-
- -
-
- - -
-
- -
- - -
diff --git a/sources/app/views/entry/bookmark.phtml b/sources/app/views/entry/bookmark.phtml index c1fc32b..c346d2c 100755 --- a/sources/app/views/entry/bookmark.phtml +++ b/sources/app/views/entry/bookmark.phtml @@ -1,16 +1,16 @@ Minz_Request::controllerName (), - 'a' => Minz_Request::actionName (), - 'params' => Minz_Request::params (), +$url = Minz_Url::display(array( + 'c' => Minz_Request::controllerName(), + 'a' => Minz_Request::actionName(), + 'params' => Minz_Request::params(), )); -echo json_encode (array ('url' => str_ireplace ('&', '&', $url), 'icon' => FreshRSS_Themes::icon(Minz_Request::param ('is_favorite') ? 'non-starred' : 'starred'))); +echo json_encode(array('url' => str_ireplace('&', '&', $url), 'icon' => _i(Minz_Request::param('is_favorite') ? 'non-starred' : 'starred'))); diff --git a/sources/app/views/entry/read.phtml b/sources/app/views/entry/read.phtml index 9e79d4c..fabdec9 100755 --- a/sources/app/views/entry/read.phtml +++ b/sources/app/views/entry/read.phtml @@ -1,16 +1,16 @@ Minz_Request::controllerName (), - 'a' => Minz_Request::actionName (), - 'params' => Minz_Request::params (), +$url = Minz_Url::display(array( + 'c' => Minz_Request::controllerName(), + 'a' => Minz_Request::actionName(), + 'params' => Minz_Request::params(), )); -echo json_encode (array ('url' => str_ireplace ('&', '&', $url), 'icon' => FreshRSS_Themes::icon(Minz_Request::param ('is_read') ? 'unread' : 'read'))); +echo json_encode(array('url' => str_ireplace('&', '&', $url), 'icon' => _i(Minz_Request::param('is_read') ? 'unread' : 'read'))); diff --git a/sources/app/views/error/index.phtml b/sources/app/views/error/index.phtml index ef4fbd3..fe3abf8 100755 --- a/sources/app/views/error/index.phtml +++ b/sources/app/views/error/index.phtml @@ -3,7 +3,7 @@

code; ?>

errorMessage; ?>
- +

diff --git a/sources/app/views/extension/configure.phtml b/sources/app/views/extension/configure.phtml new file mode 100755 index 0000000..8933eea --- /dev/null +++ b/sources/app/views/extension/configure.phtml @@ -0,0 +1,3 @@ +renderHelper('extension/configure'); diff --git a/sources/app/views/extension/index.phtml b/sources/app/views/extension/index.phtml new file mode 100755 index 0000000..f2d0502 --- /dev/null +++ b/sources/app/views/extension/index.phtml @@ -0,0 +1,44 @@ +partial('aside_configure'); ?> + +
+ + +

+ + + extension_list['system'])) { ?> +

+ extension_list['system'] as $ext) { + $this->ext_details = $ext; + $this->renderHelper('extension/details'); + } + ?> + + + extension_list['user'])) { ?> +

+ extension_list['user'] as $ext) { + $this->ext_details = $ext; + $this->renderHelper('extension/details'); + } + ?> + extension_list['system']) && empty($this->extension_list['user'])) { + ?> +

+ +
+ +extension) ? ' class="active"' : ''; ?> +> +
> +extension)) { + $this->renderHelper('extension/configure'); + } +?> +
diff --git a/sources/app/views/feed/add.phtml b/sources/app/views/feed/add.phtml index 849daca..4cdd3f3 100755 --- a/sources/app/views/feed/add.phtml +++ b/sources/app/views/feed/add.phtml @@ -1,16 +1,16 @@ feed) { ?>
-

+

load_ok) { ?> -

+

- + load_ok) { ?>
- +
@@ -18,7 +18,7 @@ feed->description(); if ($desc != '') { ?>
- +
@@ -26,26 +26,26 @@
- +
feed->website(); ?> - +
- +
- +
- +
- +
- +
- + feed->httpAuth(false); ?>
- +
- +
- +
- - + +
diff --git a/sources/app/views/feed/move.phtml b/sources/app/views/feed/move.phtml new file mode 100755 index 0000000..e69de29 diff --git a/sources/app/views/helpers/export/opml.phtml b/sources/app/views/helpers/export/opml.phtml index 8622d91..236cca3 100755 --- a/sources/app/views/helpers/export/opml.phtml +++ b/sources/app/views/helpers/export/opml.phtml @@ -2,7 +2,7 @@ $opml_array = array( 'head' => array( - 'title' => Minz_Configuration::title(), + 'title' => FreshRSS_Context::$system_conf->title, 'dateCreated' => date('D, d M Y H:i:s') ), 'body' => array() diff --git a/sources/app/views/helpers/extension/configure.phtml b/sources/app/views/helpers/extension/configure.phtml new file mode 100755 index 0000000..95d968a --- /dev/null +++ b/sources/app/views/helpers/extension/configure.phtml @@ -0,0 +1,19 @@ +
+

+ extension->getName(); ?> (extension->getVersion(); ?>) — + extension->isEnabled() ? _t('admin.extensions.enabled') + : _t('admin.extensions.disabled'); ?> +

+ +

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

+ +

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

+ +
diff --git a/sources/app/views/helpers/extension/details.phtml b/sources/app/views/helpers/extension/details.phtml new file mode 100755 index 0000000..acba4e8 --- /dev/null +++ b/sources/app/views/helpers/extension/details.phtml @@ -0,0 +1,21 @@ +
    +
  • + ext_details->getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> + ext_details->getName()); ?> +
    + + ext_details->isEnabled()) { ?> + + + + + + + +
    + + + +
  • +
  • ext_details->getName(); ?>
  • +
diff --git a/sources/app/views/helpers/feed/update.phtml b/sources/app/views/helpers/feed/update.phtml new file mode 100755 index 0000000..0b08d03 --- /dev/null +++ b/sources/app/views/helpers/feed/update.phtml @@ -0,0 +1,174 @@ +
+

feed->name(); ?>

+ +
+ + + +
+ +

feed->description(); ?>

+ + feed->nbEntries(); ?> + + feed->inError()) { ?> +

+ +

+ + +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ + +
+
+ + + +
+
+
+ + + + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ + + feed->httpAuth(false); ?> +
+ +
+ + +
+ + +
+ +
+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+
+
diff --git a/sources/app/views/helpers/index/normal/entry_bottom.phtml b/sources/app/views/helpers/index/normal/entry_bottom.phtml new file mode 100755 index 0000000..20b4b33 --- /dev/null +++ b/sources/app/views/helpers/index/normal/entry_bottom.phtml @@ -0,0 +1,86 @@ +sharing; + } + + $bottomline_read = FreshRSS_Context::$user_conf->bottomline_read; + $bottomline_favorite = FreshRSS_Context::$user_conf->bottomline_favorite; + $bottomline_sharing = FreshRSS_Context::$user_conf->bottomline_sharing && (count($sharing) > 0); + $bottomline_tags = FreshRSS_Context::$user_conf->bottomline_tags; + $bottomline_date = FreshRSS_Context::$user_conf->bottomline_date; + $bottomline_link = FreshRSS_Context::$user_conf->bottomline_link; +?>
  • 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isRead()) { + $arUrl['params']['is_read'] = 0; + } + ?>entry->isRead() ? 'read' : 'unread'); ?>
  • 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isFavorite()) { + $arUrl['params']['is_favorite'] = 0; + } + ?>entry->isFavorite() ? 'starred' : 'non-starred'); ?>
  • +
  • + +
  • entry->tags() : null; + if (!empty($tags)) { + ?>
  • + +
  • entry->date(); ?>
  • +
diff --git a/sources/app/views/helpers/index/normal/entry_header.phtml b/sources/app/views/helpers/index/normal/entry_header.phtml new file mode 100755 index 0000000..dc54429 --- /dev/null +++ b/sources/app/views/helpers/index/normal/entry_header.phtml @@ -0,0 +1,33 @@ +topline_read; + $topline_favorite = FreshRSS_Context::$user_conf->topline_favorite; + $topline_date = FreshRSS_Context::$user_conf->topline_date; + $topline_link = FreshRSS_Context::$user_conf->topline_link; +?> diff --git a/sources/app/views/helpers/javascript_vars.phtml b/sources/app/views/helpers/javascript_vars.phtml index 1139eb4..adf0783 100755 --- a/sources/app/views/helpers/javascript_vars.phtml +++ b/sources/app/views/helpers/javascript_vars.phtml @@ -1,61 +1,71 @@ +"use strict"; mark_when; +$mail = Minz_Session::param('mail', false); +$auto_actualize = Minz_Session::param('actualize_feeds', false); +$hide_posts = (FreshRSS_Context::$user_conf->display_posts || + Minz_Request::param('output') === 'reader'); +$s = FreshRSS_Context::$user_conf->shortcuts; -$mark = $this->conf->mark_when; -echo 'var ', - 'help_url="', FRESHRSS_WIKI, '"', - ',hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true', - ',display_order="', Minz_Request::param('order', $this->conf->sort_order), '"', - ',auto_mark_article=', $mark['article'] ? 'true' : 'false', - ',auto_mark_site=', $mark['site'] ? 'true' : 'false', - ',auto_mark_scroll=', $mark['scroll'] ? 'true' : 'false', - ',auto_load_more=', $this->conf->auto_load_more ? 'true' : 'false', - ',does_lazyload=', $this->conf->lazyload ? 'true' : 'false', - ',sticky_post=', $this->conf->sticky_post ? 'true' : 'false'; +$url_login = Minz_Url::display(array( + 'c' => 'auth', + 'a' => 'login' +), 'php'); +$url_logout = Minz_Url::display(array( + 'c' => 'auth', + 'a' => 'logout' +), 'php'); -$s = $this->conf->shortcuts; -echo ',shortcuts={', - 'mark_read:"', $s['mark_read'], '",', - 'mark_favorite:"', $s['mark_favorite'], '",', - 'go_website:"', $s['go_website'], '",', - 'prev_entry:"', $s['prev_entry'], '",', - 'next_entry:"', $s['next_entry'], '",', - 'first_entry:"', $s['first_entry'], '",', - 'last_entry:"', $s['last_entry'], '",', - 'collapse_entry:"', $s['collapse_entry'], '",', - 'load_more:"', $s['load_more'], '",', - 'auto_share:"', $s['auto_share'], '",', - 'focus_search:"', $s['focus_search'], '",', - 'user_filter:"', $s['user_filter'], '",', - 'help:"', $s['help'], '"', +echo 'var context={', + 'auto_remove_article:', FreshRSS_Context::isAutoRemoveAvailable() ? 'true' : 'false', ',', + 'hide_posts:', $hide_posts ? 'false' : 'true', ',', + 'display_order:"', Minz_Request::param('order', FreshRSS_Context::$user_conf->sort_order), '",', + 'auto_mark_article:', $mark['article'] ? 'true' : 'false', ',', + 'auto_mark_site:', $mark['site'] ? 'true' : 'false', ',', + 'auto_mark_scroll:', $mark['scroll'] ? 'true' : 'false', ',', + 'auto_load_more:', FreshRSS_Context::$user_conf->auto_load_more ? 'true' : 'false', ',', + 'auto_actualize_feeds:', $auto_actualize ? 'true' : 'false', ',', + 'does_lazyload:', FreshRSS_Context::$user_conf->lazyload ? 'true' : 'false', ',', + 'sticky_post:', FreshRSS_Context::isStickyPostEnabled() ? 'true' : 'false', ',', + 'html5_notif_timeout:', FreshRSS_Context::$user_conf->html5_notif_timeout, ',', + 'auth_type:"', FreshRSS_Context::$system_conf->auth_type, '",', + 'current_user_mail:', $mail ? ('"' . $mail . '"') : 'null', ',', + 'current_view:"', Minz_Request::param('output', 'normal'), '"', "},\n"; -if (Minz_Request::param ('output') === 'global') { - echo "iconClose='", FreshRSS_Themes::icon('close'), "',\n"; -} +echo 'shortcuts={', + 'mark_read:"', @$s['mark_read'], '",', + 'mark_favorite:"', @$s['mark_favorite'], '",', + 'go_website:"', @$s['go_website'], '",', + 'prev_entry:"', @$s['prev_entry'], '",', + 'next_entry:"', @$s['next_entry'], '",', + 'first_entry:"', @$s['first_entry'], '",', + 'last_entry:"', @$s['last_entry'], '",', + 'collapse_entry:"', @$s['collapse_entry'], '",', + 'load_more:"', @$s['load_more'], '",', + 'auto_share:"', @$s['auto_share'], '",', + 'focus_search:"', @$s['focus_search'], '",', + 'user_filter:"', @$s['user_filter'], '",', + 'help:"', @$s['help'], '",', + 'close_dropdown:"', @$s['close_dropdown'], '"', +"},\n"; -$authType = Minz_Configuration::authType(); -if ($authType === 'persona') { - // If user is disconnected, current_user_mail MUST be null - $mail = Minz_Session::param ('mail', false); - if ($mail) { - echo 'current_user_mail="' . $mail . '",'; - } else { - echo 'current_user_mail=null,'; - } -} +echo 'url={', + 'index:"', _url('index', 'index'), '",', + 'login:"', $url_login, '",', + 'logout:"', $url_logout, '",', + 'help:"', FRESHRSS_WIKI, '"', +"},\n"; -echo 'authType="', $authType, '",', - 'url_freshrss="', _url ('index', 'index'), '",', - 'url_login="', _url ('index', 'login'), '",', - 'url_logout="', _url ('index', 'logout'), '",'; +echo 'i18n={', + 'confirmation_default:"', _t('gen.js.confirm_action'), '",', + 'notif_title_articles:"', _t('gen.js.feedback.title_new_articles'), '",', + 'notif_body_articles:"', _t('gen.js.feedback.body_new_articles'), '",', + 'notif_request_failed:"', _t('gen.js.feedback.request_failed'), '",', + 'category_empty:"', _t('gen.js.category_empty'), '"', +"},\n"; -echo 'str_confirmation_default="', Minz_Translate::t('confirm_action'), '"', ",\n"; -echo 'str_notif_title_articles="', Minz_Translate::t('notif_title_new_articles'), '"', ",\n"; -echo 'str_notif_body_articles="', Minz_Translate::t('notif_body_new_articles'), '"', ",\n"; -echo 'html5_notif_timeout=', $this->conf->html5_notif_timeout,",\n"; - - -$autoActualise = Minz_Session::param('actualize_feeds', false); -echo 'auto_actualize_feeds=', $autoActualise ? 'true' : 'false', ";\n"; +echo 'icons={', + 'close:\'', _i('close'), '\'', +"}\n"; \ No newline at end of file diff --git a/sources/app/views/helpers/logs_pagination.phtml b/sources/app/views/helpers/logs_pagination.phtml index e3d1481..58b3c68 100755 --- a/sources/app/views/helpers/logs_pagination.phtml +++ b/sources/app/views/helpers/logs_pagination.phtml @@ -1,7 +1,7 @@ nbPage > 1) { ?> @@ -9,14 +9,14 @@
  • currentPage > 1) { ?> - « + «
  • currentPage - 1; ?>
  • currentPage > 1) { ?> - +
  • @@ -24,7 +24,7 @@ 0 && $i <= $this->nbPage) { ?> currentPage) { ?> -
  • +
  • @@ -34,13 +34,13 @@ currentPage + 1; ?>
  • currentPage < $this->nbPage) { ?> - +
  • nbPage; ?>
  • currentPage < $this->nbPage) { ?> - » + »
  • diff --git a/sources/app/views/helpers/pagination.phtml b/sources/app/views/helpers/pagination.phtml index cea3383..b20201c 100755 --- a/sources/app/views/helpers/pagination.phtml +++ b/sources/app/views/helpers/pagination.phtml @@ -1,36 +1,40 @@ 'entry', + 'a' => 'read', + 'params' => array( + 'get' => FreshRSS_Context::currentGet(), + 'nextGet' => FreshRSS_Context::$next_get, + 'idMax' => FreshRSS_Context::$id_max, + ) + ); ?>
    • - nextId)) { ?> - nextId; - $params['ajax'] = 1; - ?> - - + + + - + -
      +
    • diff --git a/sources/app/views/helpers/view/global_view.phtml b/sources/app/views/helpers/view/global_view.phtml deleted file mode 100755 index 72bcf4c..0000000 --- a/sources/app/views/helpers/view/global_view.phtml +++ /dev/null @@ -1,53 +0,0 @@ -partial ('nav_menu'); ?> - -entries)) { ?> -
      - 'index', 'a' => 'index', 'params' => array()); - if ($this->conf->view_mode !== 'normal') { - $arUrl['params']['output'] = 'normal'; - } - $p = Minz_Request::param('state', ''); - if (($p != '') && ($this->conf->default_view !== $p)) { - $arUrl['params']['state'] = $p; - } - - foreach ($this->cat_aside as $cat) { - $feeds = $cat->feeds (); - if (!empty ($feeds)) { -?> -
      - - -
      - -
      - -
      -
      conf->display_posts ? '' : ' class="hide_posts"'; ?>> - -
      - - -
      -

      -

      -
      - diff --git a/sources/app/views/helpers/view/normal_view.phtml b/sources/app/views/helpers/view/normal_view.phtml deleted file mode 100755 index 6d9789f..0000000 --- a/sources/app/views/helpers/view/normal_view.phtml +++ /dev/null @@ -1,191 +0,0 @@ -partial ('aside_flux'); -$this->partial ('nav_menu'); - -if (!empty($this->entries)) { - $display_today = true; - $display_yesterday = true; - $display_others = true; - if ($this->loginOk) { - $sharing = $this->conf->sharing; - } else { - $sharing = array(); - } - $hidePosts = !$this->conf->display_posts; - $lazyload = $this->conf->lazyload; - $topline_read = $this->conf->topline_read; - $topline_favorite = $this->conf->topline_favorite; - $topline_date = $this->conf->topline_date; - $topline_link = $this->conf->topline_link; - $bottomline_read = $this->conf->bottomline_read; - $bottomline_favorite = $this->conf->bottomline_favorite; - $bottomline_sharing = $this->conf->bottomline_sharing && (count($sharing)); - $bottomline_tags = $this->conf->bottomline_tags; - $bottomline_date = $this->conf->bottomline_date; - $bottomline_link = $this->conf->bottomline_link; - - $content_width = $this->conf->content_width; -?> - -
      - -
      entries as $item) { - if ($display_today && $item->isDay (FreshRSS_Days::TODAY, $this->today)) { - ?>
      currentName; ?>
      isDay (FreshRSS_Days::YESTERDAY, $this->today)) { - ?>
      currentName; ?>
      isDay (FreshRSS_Days::BEFORE_YESTERDAY, $this->today)) { - ?>
      currentName; ?>
      -
        loginOk) { - if ($topline_read) { - ?>
      • 'entry', 'a' => 'read', 'params' => array('id' => $item->id ())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?>isRead () ? 'read' : 'unread'); ?>
      • 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id ())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?>isFavorite () ? 'starred' : 'non-starred'); ?>
      • cat_aside, $item->feed ()); //We most likely already have the feed object in cache - if ($feed == null) { - $feed = $item->feed(true); - if ($feed == null) { - $feed = FreshRSS_Feed::example(); - } - } - ?>
      • ✇ name(); ?>
      • -
      • title (); ?>
      • -
      • date (); ?> 
      • - -
      - -
      -
      -

      title (); ?>

      - author(); - echo $author != '' ? '
      ' . Minz_Translate::t('by_author', $author) . '
      ' : '', - $lazyload && $hidePosts ? lazyimg($item->content()) : $item->content(); - ?> -
      -
        loginOk) { - if ($bottomline_read) { - ?>
      • 'entry', 'a' => 'read', 'params' => array('id' => $item->id ())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?>isRead () ? 'read' : 'unread'); ?>
      • 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id ())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?>isFavorite () ? 'starred' : 'non-starred'); ?>
      • -
      • link ()); - $title = urlencode ($item->title () . ' · ' . $feed->name ()); - ?> - -
      • tags() : null; - if (!empty($tags)) { - ?>
      • - -
      • date (); ?>
      • -
      -
      -
      - - - renderHelper('pagination'); ?> -
      - -partial ('nav_entries'); ?> - - -
      -

      -

      -
      - diff --git a/sources/app/views/helpers/view/reader_view.phtml b/sources/app/views/helpers/view/reader_view.phtml deleted file mode 100755 index c80dca5..0000000 --- a/sources/app/views/helpers/view/reader_view.phtml +++ /dev/null @@ -1,44 +0,0 @@ -partial ('nav_menu'); - -if (!empty($this->entries)) { - $lazyload = $this->conf->lazyload; - $content_width = $this->conf->content_width; -?> - -
      - entries as $item) { ?> - -
      -
      -
      - cat_aside, $item->feed ()); //We most likely already have the feed object in cache - if (empty($feed)) $feed = $item->feed (true); - ?> - - ✇ name(); ?> - -

      title (); ?>

      - -
      author(); - echo $author != '' ? Minz_Translate::t('by_author', $author) . ' — ' : '', - $item->date(); - ?>
      - - content(); ?> -
      -
      -
      - - - renderHelper('pagination'); ?> -
      - - -
      -

      -

      -
      - diff --git a/sources/app/views/importExport/index.phtml b/sources/app/views/importExport/index.phtml index 35371fa..a64524b 100755 --- a/sources/app/views/importExport/index.phtml +++ b/sources/app/views/importExport/index.phtml @@ -1,13 +1,13 @@ -partial('aside_feed'); ?> +partial('aside_subscription'); ?>
      - +
      - +
      @@ -16,24 +16,24 @@
      - +
      feeds) > 0) { ?>
      - +
      - +
      diff --git a/sources/app/views/index/about.phtml b/sources/app/views/index/about.phtml index 76ff804..3fdb516 100755 --- a/sources/app/views/index/about.phtml +++ b/sources/app/views/index/about.phtml @@ -1,27 +1,24 @@
      - + -

      +

      -
      +
      -
      -
      Marien Fressinaud
      +
      +
      -
      -
      +
      +
      -
      -
      - -
      +
      -

      +

      -

      -

      +

      +

      diff --git a/sources/app/views/index/formLogin.phtml b/sources/app/views/index/formLogin.phtml deleted file mode 100755 index b05cdce..0000000 --- a/sources/app/views/index/formLogin.phtml +++ /dev/null @@ -1,46 +0,0 @@ -
      -

      -
      - - -
      -
      - - -
      - -
      -
      - -
      -
      -
      - -
      -

      -

      - - - - - -

      - -

      -
      diff --git a/sources/app/views/index/global.phtml b/sources/app/views/index/global.phtml new file mode 100755 index 0000000..0ffa3bc --- /dev/null +++ b/sources/app/views/index/global.phtml @@ -0,0 +1,54 @@ +partial('nav_menu'); + + $class = ''; + if (FreshRSS_Context::$user_conf->hide_read_feeds && + FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ) && + !FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ)) { + $class = ' state_unread'; + } +?> + +
      + 'index', + 'a' => 'normal', + 'params' => Minz_Request::params() + ); + + foreach ($this->categories as $cat) { + $feeds = $cat->feeds(); + $url_base['params']['get'] = 'c_' . $cat->id(); + + if (!empty($feeds)) { +?> +
      + + +
        + nbNotRead(); + $error = $feed->inError() ? ' error' : ''; + $empty = $feed->nbEntries() === 0 ? ' empty' : ''; + $url_base['params']['get'] = 'f_' . $feed->id(); + ?> +
      • + ✇ + name(); ?> +
      • + +
      +
      + +
      + +
      + +
      +
      display_posts ? '' : ' class="hide_posts"'; ?>> +
      diff --git a/sources/app/views/index/index.phtml b/sources/app/views/index/index.phtml deleted file mode 100755 index 1ff36ca..0000000 --- a/sources/app/views/index/index.phtml +++ /dev/null @@ -1,25 +0,0 @@ -loginOk || Minz_Configuration::allowAnonymous()) { - if ($output === 'normal') { - $this->renderHelper ('view/normal_view'); - } elseif ($output === 'reader') { - $this->renderHelper ('view/reader_view'); - } elseif ($output === 'global') { - $this->renderHelper ('view/global_view'); - } elseif ($output === 'rss') { - $this->renderHelper ('view/rss_view'); - } else { - Minz_Request::_param ('output', 'normal'); - $output = 'normal'; - $this->renderHelper ('view/normal_view'); - } -} elseif ($output === 'rss') { - // token has already been checked in the controller so we can show the view - $this->renderHelper ('view/rss_view'); -} else { - // Normally, it should not happen, but log it anyway - Minz_Log::record ('Something is wrong in ' . __FILE__ . ' line ' . __LINE__, Minz_Log::ERROR); -} diff --git a/sources/app/views/index/login.phtml b/sources/app/views/index/login.phtml deleted file mode 100755 index cc814de..0000000 --- a/sources/app/views/index/login.phtml +++ /dev/null @@ -1 +0,0 @@ -res); ?> diff --git a/sources/app/views/index/logout.phtml b/sources/app/views/index/logout.phtml deleted file mode 100755 index a0aba93..0000000 --- a/sources/app/views/index/logout.phtml +++ /dev/null @@ -1 +0,0 @@ -OK \ No newline at end of file diff --git a/sources/app/views/index/logs.phtml b/sources/app/views/index/logs.phtml index 1b77b39..02256bd 100755 --- a/sources/app/views/index/logs.phtml +++ b/sources/app/views/index/logs.phtml @@ -1,25 +1,25 @@
      - + -

      -

      +

      +

      - +

      - logsPaginator->items (); ?> + logsPaginator->items(); ?> - +
      - logsPaginator->render ('logs_pagination.phtml', 'page'); ?> + logsPaginator->render('logs_pagination.phtml', 'page'); ?> -
      date ())); ?>info (), ENT_NOQUOTES, 'UTF-8'); ?>
      +
      date())); ?>info(), ENT_NOQUOTES, 'UTF-8'); ?>
      - logsPaginator->render ('logs_pagination.phtml','page'); ?> + logsPaginator->render('logs_pagination.phtml','page'); ?>
      -

      +

      diff --git a/sources/app/views/index/normal.phtml b/sources/app/views/index/normal.phtml new file mode 100755 index 0000000..f71abf1 --- /dev/null +++ b/sources/app/views/index/normal.phtml @@ -0,0 +1,89 @@ +partial('aside_feed'); +$this->partial('nav_menu'); + +if (!empty($this->entries)) { + $display_today = true; + $display_yesterday = true; + $display_others = true; + $hidePosts = !FreshRSS_Context::$user_conf->display_posts; + $lazyload = FreshRSS_Context::$user_conf->lazyload; + $content_width = FreshRSS_Context::$user_conf->content_width; + + $today = @strtotime('today'); +?> + +
      + +
      entries as $item) { + $this->entry = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($this->entry)) { + continue; + } + + // We most likely already have the feed object in cache + $this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feed()); + if ($this->feed == null) { + $this->feed = $this->entry->feed(true); + if ($this->feed == null) { + $this->feed = FreshRSS_Feed::example(); + } + } + + if ($display_today && $this->entry->isDay(FreshRSS_Days::TODAY, $today)) { + ?>
      entry->isDay(FreshRSS_Days::YESTERDAY, $today)) { + ?>
      entry->isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) { + ?>
      renderHelper('index/normal/entry_header'); + + ?>
      +
      +

      entry->title(); ?>

      + entry->author(); + echo $author != '' ? '
      ' . _t('gen.short.by_author', $author) . '
      ' : '', + $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content(); + ?> +
      renderHelper('index/normal/entry_bottom'); + + ?>
      +
      renderHelper('pagination'); +?>
      + +partial('nav_entries'); ?> + + +
      +

      +

      +
      + diff --git a/sources/app/views/index/reader.phtml b/sources/app/views/index/reader.phtml new file mode 100755 index 0000000..a19ee32 --- /dev/null +++ b/sources/app/views/index/reader.phtml @@ -0,0 +1,47 @@ +partial('nav_menu'); + +if (!empty($this->entries)) { + $lazyload = FreshRSS_Context::$user_conf->lazyload; + $content_width = FreshRSS_Context::$user_conf->content_width; +?> + +
      entries as $item) { + $item = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($item)) { + continue; + } + ?>
      +
      +
      + categories, $item->feed()); //We most likely already have the feed object in cache + if (empty($feed)) $feed = $item->feed(true); + ?> + + ✇ name(); ?> + +

      title(); ?>

      + +
      author(); + echo $author != '' ? _t('gen.short.by_author', $author) . ' — ' : '', + $item->date(); + ?>
      + + content(); ?> +
      +
      +
      + + + renderHelper('pagination'); ?> +
      + + +
      +

      +

      +
      + diff --git a/sources/app/views/helpers/view/rss_view.phtml b/sources/app/views/index/rss.phtml similarity index 56% rename from sources/app/views/helpers/view/rss_view.phtml rename to sources/app/views/index/rss.phtml index 2c6ca61..8607451 100755 --- a/sources/app/views/helpers/view/rss_view.phtml +++ b/sources/app/views/index/rss.phtml @@ -3,25 +3,25 @@ <?php echo $this->rss_title; ?> - rss_title); ?> + rss_title); ?> GMT - + entries as $item) { ?> - <?php echo $item->title (); ?> - link (); ?> - author (); ?> + <?php echo $item->title(); ?> + link(); ?> + author(); ?> content (); + echo $item->content(); ?>]]> - date (true)); ?> - id (); ?> + date(true)); ?> + id(); ?> diff --git a/sources/app/views/javascript/actualize.phtml b/sources/app/views/javascript/actualize.phtml index 74cef49..4542289 100755 --- a/sources/app/views/javascript/actualize.phtml +++ b/sources/app/views/javascript/actualize.phtml @@ -9,7 +9,7 @@ var feeds = [feeds as $feed) { ?>{\ -
      /
      \ +
      /
      \ 0 / " + feed_count + "\
      "); } else { @@ -23,7 +23,7 @@ function updateProgressBar(i, title_feed) { function updateFeeds() { if (feed_count === 0) { - openNotification("", "good"); + openNotification("", "good"); ajax_loading = false; return; } diff --git a/sources/app/views/stats/idle.phtml b/sources/app/views/stats/idle.phtml index 6f3d4a1..2211779 100755 --- a/sources/app/views/stats/idle.phtml +++ b/sources/app/views/stats/idle.phtml @@ -1,9 +1,9 @@ partial('aside_stats'); ?>
      - + -

      +

      -

      +

      @@ -24,13 +24,13 @@
      • - - - + + +
      • - () + ()
      @@ -42,7 +42,7 @@ if ($nothing) { ?>

      - +

      diff --git a/sources/app/views/stats/index.phtml b/sources/app/views/stats/index.phtml index fa57a77..18bcd4d 100755 --- a/sources/app/views/stats/index.phtml +++ b/sources/app/views/stats/index.phtml @@ -1,82 +1,82 @@ partial('aside_stats'); ?>
      - + -

      +

      -

      +

      - - + + - - - + + + - - - + + + - - - + + + - - - + + +
       
      repartition['main_stream']['total']); ?>repartition['all_feeds']['total']); ?>repartition['main_stream']['total']); ?>repartition['all_feeds']['total']); ?>
      repartition['main_stream']['read']); ?>repartition['all_feeds']['read']); ?>repartition['main_stream']['read']); ?>repartition['all_feeds']['read']); ?>
      repartition['main_stream']['unread']); ?>repartition['all_feeds']['unread']); ?>repartition['main_stream']['unread']); ?>repartition['all_feeds']['unread']); ?>
      repartition['main_stream']['favorite']); ?>repartition['all_feeds']['favorite']); ?>repartition['main_stream']['favorite']); ?>repartition['all_feeds']['favorite']); ?>
      -

      +

      - - - - + + + + - topFeed as $feed): ?> + topFeed as $feed) { ?> - - + + - +
      repartition['all_feeds']['total'] * 100, 1);?>repartition['all_feeds']['total'] * 100, 1);?>
      -

      +

      -

      +

      -

      +

      diff --git a/sources/app/views/stats/repartition.phtml b/sources/app/views/stats/repartition.phtml index 750a3ff..b20d9bb 100755 --- a/sources/app/views/stats/repartition.phtml +++ b/sources/app/views/stats/repartition.phtml @@ -1,12 +1,12 @@ partial('aside_stats'); ?>
      - + -

      +

      - + categories as $category) { $feeds = $category->feeds(); if (!empty($feeds)) { @@ -24,23 +24,40 @@ feed) {?> - - + +
      -

      + + + + + + + + + + + + + +
      repartition['total']; ?>repartition['read']; ?>repartition['unread']; ?>repartition['favorite']; ?>
      +
      + +
      +

      averageHour); ?>

      -

      +

      averageDayOfWeek); ?>

      -

      +

      averageMonth); ?>

      @@ -56,19 +73,10 @@ function initStats() { return; } // Entry per hour - var avg_h = []; - for (var i = -1; i <= 24; i++) { - avg_h.push([i, averageHour?>]); - } Flotr.draw(document.getElementById('statsEntryPerHour'), [{ data: repartitionHour ?>, bars: {horizontal: false, show: true} - }, { - data: avg_h, - lines: {show: true}, - label: "averageHour?>", - yaxis: 2 }], { grid: {verticalLines: false}, @@ -81,23 +89,13 @@ function initStats() { max: 23.9, tickDecimals: 0}, yaxis: {min: 0}, - y2axis: {showLabels: false}, mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}} }); // Entry per day of week - var avg_dow = []; - for (var i = -1; i <= 7; i++) { - avg_dow.push([i, averageDayOfWeek?>]); - } Flotr.draw(document.getElementById('statsEntryPerDayOfWeek'), [{ data: repartitionDayOfWeek ?>, bars: {horizontal: false, show: true} - }, { - data: avg_dow, - lines: {show: true}, - label: "averageDayOfWeek?>", - yaxis: 2 }], { grid: {verticalLines: false}, @@ -111,23 +109,13 @@ function initStats() { max: 6.9, tickDecimals: 0}, yaxis: {min: 0}, - y2axis: {showLabels: false}, mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}} }); // Entry per month - var avg_m = []; - for (var i = 0; i <= 13; i++) { - avg_m.push([i, averageMonth?>]); - } Flotr.draw(document.getElementById('statsEntryPerMonth'), [{ data: repartitionMonth ?>, bars: {horizontal: false, show: true} - }, { - data: avg_m, - lines: {show: true}, - label: "averageMonth?>", - yaxis: 2 }], { grid: {verticalLines: false}, @@ -141,7 +129,6 @@ function initStats() { max: 12.9, tickDecimals: 0}, yaxis: {min: 0}, - y2axis: {showLabels: false}, mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}} }); diff --git a/sources/app/views/subscription/feed.phtml b/sources/app/views/subscription/feed.phtml new file mode 100755 index 0000000..60664fd --- /dev/null +++ b/sources/app/views/subscription/feed.phtml @@ -0,0 +1,15 @@ +partial('aside_subscription'); +} + +if ($this->feed) { + $this->renderHelper('feed/update'); +} else { +?> +
      + + +
      + diff --git a/sources/app/views/subscription/index.phtml b/sources/app/views/subscription/index.phtml new file mode 100755 index 0000000..331e824 --- /dev/null +++ b/sources/app/views/subscription/index.phtml @@ -0,0 +1,148 @@ +partial('aside_subscription'); ?> + +
      + + +

      + +
      +
      + + + +
      +
      + +

      + default_category->name()); ?> +

      + +
      +
      + +
        +
        +
      • +
      • +
        +
      +
      + + + + categories as $cat) { + $feeds = $cat->feeds(); + ?> +
      +
      +
      + + + +
      +
      + +
        + + inError() ? ' error' : ''; + $empty = $feed->nbEntries() == 0 ? ' empty' : ''; + ?> +
      • + + ✇ name(); ?> +
      • + +
      • + +
      +
      + +
      + +feed) ? ' class="active"' : ''; ?> +> +
      > +feed)) { + $this->renderHelper('feed/update'); + } +?> +
      diff --git a/sources/app/views/update/apply.phtml b/sources/app/views/update/apply.phtml index 30566c7..8221929 100755 --- a/sources/app/views/update/apply.phtml +++ b/sources/app/views/update/apply.phtml @@ -1,9 +1,9 @@ partial('aside_configure'); ?>
      - + -

      +

      diff --git a/sources/app/views/update/checkInstall.phtml b/sources/app/views/update/checkInstall.phtml new file mode 100755 index 0000000..a92860c --- /dev/null +++ b/sources/app/views/update/checkInstall.phtml @@ -0,0 +1,38 @@ +partial('aside_configure'); ?> + +
      + + +

      + + status_php as $key => $status) { ?> +

      + +

      + + +

      + + status_files as $key => $status) { ?> +

      + +

      + + + + + status_database as $key => $status) { ?> +

      + +

      + + */ ?> + +
      diff --git a/sources/app/views/update/index.phtml b/sources/app/views/update/index.phtml index 401f6ac..da1bc7e 100755 --- a/sources/app/views/update/index.phtml +++ b/sources/app/views/update/index.phtml @@ -1,12 +1,16 @@ partial('aside_configure'); ?>
      - + -

      +

      - last_update_time); ?> + +

      + +

      + last_update_time); ?>

      message)) { ?> @@ -14,23 +18,17 @@ message['title']; ?> message['body']; ?>

      - check_last_hour) { ?> -

      - - -

      check_last_hour && - (empty($this->message) || $this->message['status'] !== 'good')) { + if (empty($this->message) || $this->message['status'] !== 'good') { ?>

      - +

      update_to_apply) { ?> - +
      diff --git a/sources/app/views/user/manage.phtml b/sources/app/views/user/manage.phtml new file mode 100755 index 0000000..fe1b661 --- /dev/null +++ b/sources/app/views/user/manage.phtml @@ -0,0 +1,80 @@ +partial('aside_configure'); ?> + +
      + + +
      + + +
      + +
      + +
      +
      + +
      + +
      + +
      +
      + +
      + +
      +
      + + +
      + + +
      +
      + +
      + + mail_login; ?> +
      + +
      +
      + +
      +
      + + +
      +
      +
      + +
      + + +
      + +
      + + +

      nb_articles), + format_bytes($this->size_user)); ?>

      +
      +
      + +
      +
      + +
      +
      +
      +
      diff --git a/sources/app/views/user/profile.phtml b/sources/app/views/user/profile.phtml new file mode 100755 index 0000000..c44202e --- /dev/null +++ b/sources/app/views/user/profile.phtml @@ -0,0 +1,60 @@ +partial('aside_configure'); ?> + +
      + + +
      + + +
      + +
      + + +
      +
      + +
      + +
      +
      + /> + +
      + + +
      +
      + + api_enabled) { ?> +
      + +
      +
      + /> + +
      +
      +
      + + +
      + + mail_login; ?> +
      + placeholder="alice@example.net" /> + +
      +
      + +
      +
      + + +
      +
      +
      +
      diff --git a/sources/constants.php b/sources/constants.php index adc9652..4ee9370 100755 --- a/sources/constants.php +++ b/sources/constants.php @@ -1,5 +1,5 @@ 'production', + 'salt' => '', + 'base_url' => '', + 'language' => 'en', + 'title' => 'FreshRSS', + 'default_user' => '_', + 'allow_anonymous' => false, + 'allow_anonymous_refresh' => false, + 'auth_type' => 'none', + 'api_enabled' => false, + 'unsafe_autologin_enabled' => false, + 'limits' => array( + 'cache_duration' => 800, + 'timeout' => 10, + 'max_inactivity' => PHP_INT_MAX, + 'max_feeds' => 16384, + 'max_categories' => 16384, + ), + 'db' => array( + 'type' => 'sqlite', + 'host' => '', + 'user' => '', + 'password' => '', + 'base' => '', + 'prefix' => '', + ), + 'extensions_enabled' => array(), +); diff --git a/sources/data/log/.gitignore b/sources/data/log/.gitignore deleted file mode 100755 index bf0824e..0000000 --- a/sources/data/log/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.log \ No newline at end of file diff --git a/sources/data/shares.php b/sources/data/shares.php index 44176f1..6e0e9ea 100755 --- a/sources/data/shares.php +++ b/sources/data/shares.php @@ -19,7 +19,7 @@ return array( 'shaarli' => array( 'url' => '~URL~?post=~LINK~&title=~TITLE~&source=FreshRSS', - 'transform' => array('urlencode'), + 'transform' => array('rawurlencode'), 'help' => 'http://sebsauvage.net/wiki/doku.php?id=php:shaarli', 'form' => 'advanced', ), @@ -40,31 +40,28 @@ return array( ), 'diaspora' => array( 'url' => '~URL~/bookmarklet?url=~LINK~&title=~TITLE~', - 'transform' => array('urlencode'), + 'transform' => array('rawurlencode'), 'help' => 'https://diasporafoundation.org/', 'form' => 'advanced', ), 'twitter' => array( 'url' => 'https://twitter.com/share?url=~LINK~&text=~TITLE~', - 'transform' => array('urlencode'), + 'transform' => array('rawurlencode'), 'form' => 'simple', ), 'g+' => array( 'url' => 'https://plus.google.com/share?url=~LINK~', - 'transform' => array('urlencode'), + 'transform' => array('rawurlencode'), 'form' => 'simple', ), 'facebook' => array( 'url' => 'https://www.facebook.com/sharer.php?u=~LINK~&t=~TITLE~', - 'transform' => array('urlencode'), + 'transform' => array('rawurlencode'), 'form' => 'simple', ), 'email' => array( 'url' => 'mailto:?subject=~TITLE~&body=~LINK~', - 'transform' => array( - 'link' => array('urlencode'), - 'title' => array(), - ), + 'transform' => array('rawurlencode'), 'form' => 'simple', ), 'print' => array( diff --git a/sources/data/users/.gitignore b/sources/data/users/.gitignore new file mode 100755 index 0000000..a8b7cd6 --- /dev/null +++ b/sources/data/users/.gitignore @@ -0,0 +1,4 @@ +db.sqlite +config.php +log*.txt + diff --git a/sources/data/users/_/config.default.php b/sources/data/users/_/config.default.php new file mode 100755 index 0000000..6d3f73a --- /dev/null +++ b/sources/data/users/_/config.default.php @@ -0,0 +1,67 @@ + 'en', + 'old_entries' => 3, + 'keep_history_default' => 0, + 'ttl_default' => 3600, + 'mail_login' => '', + 'token' => '', + 'passwordHash' => '', + 'apiPasswordHash' => '', + 'posts_per_page' => 20, + 'view_mode' => 'normal', + 'default_view' => 'adaptive', + 'default_state' => FreshRSS_Entry::STATE_NOT_READ, + 'auto_load_more' => true, + 'display_posts' => false, + 'display_categories' => false, + 'hide_read_feeds' => true, + 'onread_jump_next' => true, + 'lazyload' => true, + 'sticky_post' => true, + 'reading_confirm' => false, + 'auto_remove_article' => false, + 'sort_order' => 'DESC', + 'anon_access' => false, + 'mark_when' => array ( + 'article' => true, + 'site' => true, + 'scroll' => false, + 'reception' => false, + ), + 'theme' => 'Origine', + 'content_width' => 'thin', + 'shortcuts' => array ( + 'mark_read' => 'r', + 'mark_favorite' => 'f', + 'go_website' => 'space', + 'next_entry' => 'j', + 'prev_entry' => 'k', + 'first_entry' => 'home', + 'last_entry' => 'end', + 'collapse_entry' => 'c', + 'load_more' => 'm', + 'auto_share' => 's', + 'focus_search' => 'a', + 'user_filter' => 'u', + 'help' => 'f1', + 'close_dropdown' => 'escape', + ), + 'topline_read' => true, + 'topline_favorite' => true, + 'topline_date' => true, + 'topline_link' => true, + 'bottomline_read' => true, + 'bottomline_favorite' => true, + 'bottomline_sharing' => true, + 'bottomline_tags' => true, + 'bottomline_date' => true, + 'bottomline_link' => true, + 'sharing' => array ( + ), + 'queries' => array ( + ), + 'html5_notif_timeout' => 0, + 'extensions_enabled' => array(), +); diff --git a/sources/data/log/index.html b/sources/data/users/index.html similarity index 100% rename from sources/data/log/index.html rename to sources/data/users/index.html diff --git a/sources/extensions/.gitignore b/sources/extensions/.gitignore new file mode 100755 index 0000000..cd55920 --- /dev/null +++ b/sources/extensions/.gitignore @@ -0,0 +1 @@ +[xX]* diff --git a/sources/extensions/README.md b/sources/extensions/README.md new file mode 100755 index 0000000..4c1a81f --- /dev/null +++ b/sources/extensions/README.md @@ -0,0 +1,3 @@ +# FreshRSS extensions + +You may place in this directory some custom extensions for FreshRSS. diff --git a/sources/lib/Favicon/DataAccess.php b/sources/lib/Favicon/DataAccess.php new file mode 100755 index 0000000..2bfdf64 --- /dev/null +++ b/sources/lib/Favicon/DataAccess.php @@ -0,0 +1,40 @@ +set_context(); + return @file_get_contents($url); + } + + public function retrieveHeader($url) { + $this->set_context(); + return @get_headers($url, TRUE); + } + + public function saveCache($file, $data) { + file_put_contents($file, $data); + } + + public function readCache($file) { + return file_get_contents($file); + } + + private function set_context() { + stream_context_set_default( + array( + 'http' => array( + 'method' => 'GET', + 'timeout' => 10, + 'header' => "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:20.0; Favicon; +https://github.com/ArthurHoaro/favicon) Gecko/20100101 Firefox/32.0\r\n", + ) + ) + ); + } +} \ No newline at end of file diff --git a/sources/lib/Favicon/Favicon.php b/sources/lib/Favicon/Favicon.php new file mode 100755 index 0000000..7ea6ccf --- /dev/null +++ b/sources/lib/Favicon/Favicon.php @@ -0,0 +1,293 @@ +url = $args['url']; + } + + $this->cacheDir = __DIR__ . '/../../resources/cache'; + $this->dataAccess = new DataAccess(); + } + + public function cache($args = array()) { + if (isset($args['dir'])) { + $this->cacheDir = $args['dir']; + } + + if (!empty($args['timeout'])) { + $this->cacheTimeout = $args['timeout']; + } else { + $this->cacheTimeout = 0; + } + } + + public static function baseUrl($url, $path = false) + { + $return = ''; + + if (!$url = parse_url($url)) { + return FALSE; + } + + // Scheme + $scheme = isset($url['scheme']) ? strtolower($url['scheme']) : null; + if ($scheme != 'http' && $scheme != 'https') { + + return FALSE; + } + $return .= "{$scheme}://"; + + // Username and password + if (isset($url['user'])) { + $return .= $url['user']; + if (isset($url['pass'])) { + $return .= ":{$url['pass']}"; + } + $return .= '@'; + } + + // Hostname + if( !isset($url['host']) ) { + return FALSE; + } + + $return .= $url['host']; + + // Port + if (isset($url['port'])) { + $return .= ":{$url['port']}"; + } + + // Path + if( $path && isset($url['path']) ) { + $return .= $url['path']; + } + $return .= '/'; + + return $return; + } + + public function info($url) + { + if(empty($url) || $url === false) { + return false; + } + + $max_loop = 5; + + // Discover real status by following redirects. + $loop = TRUE; + while ($loop && $max_loop-- > 0) { + $headers = $this->dataAccess->retrieveHeader($url); + $exploded = explode(' ', $headers[0]); + + if( !isset($exploded[1]) ) { + return false; + } + list(,$status) = $exploded; + + switch ($status) { + case '301': + case '302': + $url = $headers['Location']; + break; + default: + $loop = FALSE; + break; + } + } + + return array('status' => $status, 'url' => $url); + } + + public function endRedirect($url) { + $out = $this->info($url); + return !empty($out['url']) ? $out['url'] : false; + } + + /** + * Find remote (or cached) favicon + * @return favicon URL, false if nothing was found + **/ + public function get($url = '') + { + // URLs passed to this method take precedence. + if (!empty($url)) { + $this->url = $url; + } + + // Get the base URL without the path for clearer concatenations. + $original = rtrim($this->baseUrl($this->url, true), '/'); + $url = rtrim($this->endRedirect($this->baseUrl($this->url, false)), '/'); + + if(($favicon = $this->checkCache($url)) || ($favicon = $this->getFavicon($url))) { + $base = true; + } + elseif(($favicon = $this->checkCache($original)) || ($favicon = $this->getFavicon($original, false))) { + $base = false; + } + else + return false; + + // Save cache if necessary + $cache = $this->cacheDir . '/' . md5($base ? $url : $original); + if ($this->cacheTimeout && !file_exists($cache) || (is_writable($cache) && time() - filemtime($cache) > $this->cacheTimeout)) { + $this->dataAccess->saveCache($cache, $favicon); + } + + return $favicon; + } + + private function getFavicon($url, $checkDefault = true) { + $favicon = false; + + if(empty($url)) { + return false; + } + + // Try /favicon.ico first. + if( $checkDefault ) { + $info = $this->info("{$url}/favicon.ico"); + if ($info['status'] == '200') { + $favicon = $info['url']; + } + } + + // See if it's specified in a link tag in domain url. + if (!$favicon) { + $favicon = $this->getInPage($url); + } + + // Make sure the favicon is an absolute URL. + if( $favicon && filter_var($favicon, FILTER_VALIDATE_URL) === false ) { + $favicon = $url . '/' . $favicon; + } + + // Sometimes people lie, so check the status. + // And sometimes, it's not even an image. Sneaky bastards! + // If cacheDir isn't writable, that's not our problem + if ($favicon && is_writable($this->cacheDir) && !$this->checkImageMType($favicon)) { + $favicon = false; + } + + return $favicon; + } + + private function getInPage($url) { + $html = $this->dataAccess->retrieveUrl("{$url}/"); + preg_match('!.*!ims', $html, $match); + + if(empty($match) || count($match) == 0) { + return false; + } + + $head = $match[0]; + + $dom = new \DOMDocument(); + // Use error supression, because the HTML might be too malformed. + if (@$dom->loadHTML($head)) { + $links = $dom->getElementsByTagName('link'); + foreach ($links as $link) { + if ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'shortcut icon') { + return $link->getAttribute('href'); + } elseif ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'icon') { + return $link->getAttribute('href'); + } elseif ($link->hasAttribute('href') && strpos($link->getAttribute('href'), 'favicon') !== FALSE) { + return $link->getAttribute('href'); + } + } + } + return false; + } + + private function checkCache($url) { + if ($this->cacheTimeout) { + $cache = $this->cacheDir . '/' . md5($url); + if (file_exists($cache) && is_readable($cache) && (time() - filemtime($cache) < $this->cacheTimeout)) { + return $this->dataAccess->readCache($cache); + } + } + return false; + } + + private function checkImageMType($url) { + $tmpFile = $this->cacheDir . '/tmp.ico'; + + $fileContent = $this->dataAccess->retrieveUrl($url); + $this->dataAccess->saveCache($tmpFile, $fileContent); + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $isImage = strpos(finfo_file($finfo, $tmpFile), 'image') !== false; + finfo_close($finfo); + + unlink($tmpFile); + + return $isImage; + } + + /** + * @return mixed + */ + public function getCacheDir() + { + return $this->cacheDir; + } + + /** + * @param mixed $cacheDir + */ + public function setCacheDir($cacheDir) + { + $this->cacheDir = $cacheDir; + } + + /** + * @return mixed + */ + public function getCacheTimeout() + { + return $this->cacheTimeout; + } + + /** + * @param mixed $cacheTimeout + */ + public function setCacheTimeout($cacheTimeout) + { + $this->cacheTimeout = $cacheTimeout; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @param string $url + */ + public function setUrl($url) + { + $this->url = $url; + } + + /** + * @param DataAccess $dataAccess + */ + public function setDataAccess($dataAccess) + { + $this->dataAccess = $dataAccess; + } +} diff --git a/sources/lib/Minz/BadConfigurationException.php b/sources/lib/Minz/BadConfigurationException.php deleted file mode 100755 index a7b77d6..0000000 --- a/sources/lib/Minz/BadConfigurationException.php +++ /dev/null @@ -1,9 +0,0 @@ - -*/ /** - * La classe Configuration permet de gérer la configuration de l'application + * Manage configuration for the application. */ class Minz_Configuration { - const CONF_PATH_NAME = '/config.php'; + /** + * The list of configurations. + */ + private static $config_list = array(); /** - * VERSION est la version actuelle de MINZ + * Add a new configuration to the list of configuration. + * + * @param $namespace the name of the current configuration + * @param $config_filename the filename of the configuration + * @param $default_filename a filename containing default values for the configuration + * @param $configuration_setter an optional helper to set values in configuration */ - const VERSION = '1.3.1.freshrss'; // version spéciale FreshRSS - - /** - * valeurs possibles pour l'"environment" - * SILENT rend l'application muette (pas de log) - * PRODUCTION est recommandée pour une appli en production - * (log les erreurs critiques) - * DEVELOPMENT log toutes les erreurs - */ - const SILENT = 0; - const PRODUCTION = 1; - const DEVELOPMENT = 2; - - /** - * définition des variables de configuration - * $salt une chaîne de caractères aléatoires (obligatoire) - * $environment gère le niveau d'affichage pour log et erreurs - * $base_url le chemin de base pour accéder à l'application - * $title le nom de l'application - * $language la langue par défaut de l'application - * $db paramètres pour la base de données (tableau) - * - host le serveur de la base - * - user nom d'utilisateur - * - password mot de passe de l'utilisateur - * - base le nom de la base de données - */ - private static $salt = ''; - private static $environment = Minz_Configuration::PRODUCTION; - private static $base_url = ''; - private static $title = ''; - private static $language = 'en'; - private static $default_user = ''; - private static $allow_anonymous = false; - private static $allow_anonymous_refresh = false; - private static $auth_type = 'none'; - private static $api_enabled = false; - private static $unsafe_autologin_enabled = false; - - private static $db = array ( - 'type' => 'mysql', - 'host' => '', - 'user' => '', - 'password' => '', - 'base' => '', - 'prefix' => '', - ); - - /* - * Getteurs - */ - public static function salt () { - return self::$salt; - } - public static function environment ($str = false) { - $env = self::$environment; - - if ($str) { - switch (self::$environment) { - case self::SILENT: - $env = 'silent'; - break; - case self::DEVELOPMENT: - $env = 'development'; - break; - case self::PRODUCTION: - default: - $env = 'production'; - } - } - - return $env; - } - public static function baseUrl () { - return self::$base_url; - } - public static function title () { - return self::$title; - } - public static function language () { - return self::$language; - } - public static function dataBase () { - return self::$db; - } - public static function defaultUser () { - return self::$default_user; - } - public static function isAdmin($currentUser) { - return $currentUser === self::$default_user; - } - public static function allowAnonymous() { - return self::$allow_anonymous; - } - public static function allowAnonymousRefresh() { - return self::$allow_anonymous_refresh; - } - public static function authType() { - return self::$auth_type; - } - public static function needsLogin() { - return self::$auth_type !== 'none'; - } - public static function canLogIn() { - return self::$auth_type === 'form' || self::$auth_type === 'persona'; - } - public static function apiEnabled() { - return self::$api_enabled; - } - public static function unsafeAutologinEnabled() { - return self::$unsafe_autologin_enabled; - } - - public static function _allowAnonymous($allow = false) { - self::$allow_anonymous = ((bool)$allow) && self::canLogIn(); - } - public static function _allowAnonymousRefresh($allow = false) { - self::$allow_anonymous_refresh = ((bool)$allow) && self::allowAnonymous(); - } - public static function _authType($value) { - $value = strtolower($value); - switch ($value) { - case 'form': - case 'http_auth': - case 'persona': - case 'none': - self::$auth_type = $value; - break; - } - self::_allowAnonymous(self::$allow_anonymous); - } - - public static function _enableApi($value = false) { - self::$api_enabled = (bool)$value; - } - public static function _enableAutologin($value = false) { - self::$unsafe_autologin_enabled = (bool)$value; - } - - /** - * Initialise les variables de configuration - * @exception Minz_FileNotExistException si le CONF_PATH_NAME n'existe pas - * @exception Minz_BadConfigurationException si CONF_PATH_NAME mal formaté - */ - public static function init () { - try { - self::parseFile (); - self::setReporting (); - } catch (Minz_FileNotExistException $e) { - throw $e; - } catch (Minz_BadConfigurationException $e) { - throw $e; - } - } - - public static function writeFile() { - $ini_array = array( - 'general' => array( - 'environment' => self::environment(true), - 'salt' => self::$salt, - 'base_url' => self::$base_url, - 'title' => self::$title, - 'default_user' => self::$default_user, - 'allow_anonymous' => self::$allow_anonymous, - 'allow_anonymous_refresh' => self::$allow_anonymous_refresh, - 'auth_type' => self::$auth_type, - 'api_enabled' => self::$api_enabled, - 'unsafe_autologin_enabled' => self::$unsafe_autologin_enabled, - ), - 'db' => self::$db, + public static function register($namespace, $config_filename, $default_filename = null, + $configuration_setter = null) { + self::$config_list[$namespace] = new Minz_Configuration( + $namespace, $config_filename, $default_filename, $configuration_setter ); - @rename(DATA_PATH . self::CONF_PATH_NAME, DATA_PATH . self::CONF_PATH_NAME . '.bak.php'); - $result = file_put_contents(DATA_PATH . self::CONF_PATH_NAME, "= 0 && - $general['environment'] <= 2) { - // fallback 0.7-beta - self::$environment = $general['environment']; - } else { - throw new Minz_BadConfigurationException ( - 'environment', - Minz_Exception::ERROR - ); - } - } - - } - if (isset ($general['base_url'])) { - self::$base_url = $general['base_url']; - } - - if (isset ($general['title'])) { - self::$title = $general['title']; - } - if (isset ($general['language'])) { - self::$language = $general['language']; - } - if (isset ($general['default_user'])) { - self::$default_user = $general['default_user']; - } - if (isset ($general['auth_type'])) { - self::_authType($general['auth_type']); - } - if (isset ($general['allow_anonymous'])) { - self::$allow_anonymous = ( - ((bool)($general['allow_anonymous'])) && - ($general['allow_anonymous'] !== 'no') - ); - } - if (isset ($general['allow_anonymous_refresh'])) { - self::$allow_anonymous_refresh = ( - ((bool)($general['allow_anonymous_refresh'])) && - ($general['allow_anonymous_refresh'] !== 'no') - ); - } - if (isset ($general['api_enabled'])) { - self::$api_enabled = ( - ((bool)($general['api_enabled'])) && - ($general['api_enabled'] !== 'no') - ); - } - if (isset ($general['unsafe_autologin_enabled'])) { - self::$unsafe_autologin_enabled = ( - ((bool)($general['unsafe_autologin_enabled'])) && - ($general['unsafe_autologin_enabled'] !== 'no') - ); - } - - // Base de données - if (isset ($ini_array['db'])) { - $db = $ini_array['db']; - if (empty($db['type'])) { - throw new Minz_BadConfigurationException ( - 'type', - Minz_Exception::ERROR - ); - } - switch ($db['type']) { - case 'mysql': - if (empty($db['host'])) { - throw new Minz_BadConfigurationException ( - 'host', - Minz_Exception::ERROR - ); - } - if (empty($db['user'])) { - throw new Minz_BadConfigurationException ( - 'user', - Minz_Exception::ERROR - ); - } - if (!isset($db['password'])) { - throw new Minz_BadConfigurationException ( - 'password', - Minz_Exception::ERROR - ); - } - if (empty($db['base'])) { - throw new Minz_BadConfigurationException ( - 'base', - Minz_Exception::ERROR - ); - } - self::$db['host'] = $db['host']; - self::$db['user'] = $db['user']; - self::$db['password'] = $db['password']; - self::$db['base'] = $db['base']; - if (isset($db['prefix'])) { - self::$db['prefix'] = $db['prefix']; - } - break; - case 'sqlite': - self::$db['host'] = ''; - self::$db['user'] = ''; - self::$db['password'] = ''; - self::$db['base'] = ''; - self::$db['prefix'] = ''; - break; - default: - throw new Minz_BadConfigurationException ( - 'type', - Minz_Exception::ERROR - ); - break; - } - self::$db['type'] = $db['type']; + $data = @include($filename); + if (is_array($data)) { + return $data; + } else { + return array(); } } - private static function setReporting() { - switch (self::$environment) { - case self::PRODUCTION: - error_reporting(E_ALL); - ini_set('display_errors','Off'); - ini_set('log_errors', 'On'); - break; - case self::DEVELOPMENT: - error_reporting(E_ALL); - ini_set('display_errors','On'); - ini_set('log_errors', 'On'); - break; - case self::SILENT: - error_reporting(0); - break; + /** + * Return the configuration related to a given namespace. + * + * @param $namespace the name of the configuration to get. + * @return a Minz_Configuration object + * @throws Minz_ConfigurationNamespaceException if the namespace does not exist. + */ + public static function get($namespace) { + if (!isset(self::$config_list[$namespace])) { + throw new Minz_ConfigurationNamespaceException( + $namespace . ' namespace does not exist' + ); } + + return self::$config_list[$namespace]; + } + + /** + * The namespace of the current configuration. + */ + private $namespace = ''; + + /** + * The filename for the current configuration. + */ + private $config_filename = ''; + + /** + * The filename for the current default values, null by default. + */ + private $default_filename = null; + + /** + * The configuration values, an empty array by default. + */ + private $data = array(); + + /** + * The default values, an empty array by default. + */ + private $data_default = array(); + + /** + * An object which help to set good values in configuration. + */ + private $configuration_setter = null; + + public function removeExtension($ext_name) { + self::$extensions_enabled = array_diff( + self::$extensions_enabled, + array($ext_name) + ); + } + public function addExtension($ext_name) { + $found = array_search($ext_name, self::$extensions_enabled) !== false; + if (!$found) { + self::$extensions_enabled[] = $ext_name; + } + } + + /** + * Create a new Minz_Configuration object. + * + * @param $namespace the name of the current configuration. + * @param $config_filename the file containing configuration values. + * @param $default_filename the file containing default values, null by default. + * @param $configuration_setter an optional helper to set values in configuration + */ + private function __construct($namespace, $config_filename, $default_filename = null, + $configuration_setter = null) { + $this->namespace = $namespace; + $this->config_filename = $config_filename; + + try { + $this->data = self::load($this->config_filename); + } catch (Minz_FileNotExistException $e) { + if (is_null($default_filename)) { + throw $e; + } + } + + $this->default_filename = $default_filename; + if (!is_null($this->default_filename)) { + $this->data_default = self::load($this->default_filename); + } + + $this->_configurationSetter($configuration_setter); + } + + /** + * Set a configuration setter for the current configuration. + * @param $configuration_setter the setter to call when modifying data. It + * must implement an handle($key, $value) method. + */ + public function _configurationSetter($configuration_setter) { + if (is_callable(array($configuration_setter, 'handle'))) { + $this->configuration_setter = $configuration_setter; + } + } + + /** + * Return the value of the given param. + * + * @param $key the name of the param. + * @param $default default value to return if key does not exist. + * @return the value corresponding to the key. + * @throws Minz_ConfigurationParamException if the param does not exist + */ + public function param($key, $default = null) { + if (isset($this->data[$key])) { + return $this->data[$key]; + } elseif (!is_null($default)) { + return $default; + } elseif (isset($this->data_default[$key])) { + return $this->data_default[$key]; + } else { + Minz_Log::warning($key . ' does not exist in configuration'); + return null; + } + } + + /** + * A wrapper for param(). + */ + public function __get($key) { + return $this->param($key); + } + + /** + * Set or remove a param. + * + * @param $key the param name to set. + * @param $value the value to set. If null, the key is removed from the configuration. + */ + public function _param($key, $value = null) { + if (!is_null($this->configuration_setter) && $this->configuration_setter->support($key)) { + $this->configuration_setter->handle($this->data, $key, $value); + } elseif (isset($this->data[$key]) && is_null($value)) { + unset($this->data[$key]); + } elseif (!is_null($value)) { + $this->data[$key] = $value; + } + } + + /** + * A wrapper for _param(). + */ + public function __set($key, $value) { + $this->_param($key, $value); + } + + /** + * Save the current configuration in the configuration file. + */ + public function save() { + $back_filename = $this->config_filename . '.bak.php'; + @rename($this->config_filename, $back_filename); + + if (file_put_contents($this->config_filename, + "data, true) . ';', + LOCK_EX) === false) { + return false; + } + + // Clear PHP 5.5+ cache for include + if (function_exists('opcache_invalidate')) { + opcache_invalidate($this->config_filename); + } + + return true; } } diff --git a/sources/lib/Minz/ConfigurationException.php b/sources/lib/Minz/ConfigurationException.php new file mode 100755 index 0000000..f294c33 --- /dev/null +++ b/sources/lib/Minz/ConfigurationException.php @@ -0,0 +1,8 @@ +createController ('FreshRSS_' . Minz_Request::controllerName () . '_Controller'); + $this->createController (Minz_Request::controllerName ()); $this->controller->init (); $this->controller->firstAction (); if (!self::$needsReset) { @@ -67,14 +68,18 @@ class Minz_Dispatcher { /** * Instancie le Controller - * @param $controller_name le nom du controller à instancier + * @param $base_name le nom du controller à instancier * @exception ControllerNotExistException le controller n'existe pas * @exception ControllerNotActionControllerException controller n'est * > pas une instance de ActionController */ - private function createController ($controller_name) { - $filename = APP_PATH . self::CONTROLLERS_PATH_NAME . '/' - . $controller_name . '.php'; + private function createController ($base_name) { + if (self::isRegistered($base_name)) { + self::loadController($base_name); + $controller_name = 'FreshExtension_' . $base_name . '_Controller'; + } else { + $controller_name = 'FreshRSS_' . $base_name . '_Controller'; + } if (!class_exists ($controller_name)) { throw new Minz_ControllerNotExistException ( @@ -114,4 +119,42 @@ class Minz_Dispatcher { $action_name )); } + + /** + * Register a controller file. + * + * @param $base_name the base name of the controller (i.e. ./?c=) + * @param $base_path the base path where we should look into to find info. + */ + public static function registerController($base_name, $base_path) { + if (!self::isRegistered($base_name)) { + self::$registrations[$base_name] = $base_path; + } + } + + /** + * Return if a controller is registered. + * + * @param $base_name the base name of the controller. + * @return true if the controller has been registered, false else. + */ + public static function isRegistered($base_name) { + return isset(self::$registrations[$base_name]); + } + + /** + * Load a controller file (include). + * + * @param $base_name the base name of the controller. + */ + private static function loadController($base_name) { + $base_path = self::$registrations[$base_name]; + $controller_filename = $base_path . '/controllers/' . $base_name . 'Controller.php'; + include_once $controller_filename; + } + + private static function setViewPath($controller, $base_name) { + $base_path = self::$registrations[$base_name]; + $controller->view()->setBasePathname($base_path); + } } diff --git a/sources/lib/Minz/Error.php b/sources/lib/Minz/Error.php index c8222a4..3e4a3e8 100755 --- a/sources/lib/Minz/Error.php +++ b/sources/lib/Minz/Error.php @@ -19,46 +19,17 @@ class Minz_Error { * > $logs['notice'] * @param $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis) */ - public static function error ($code = 404, $logs = array (), $redirect = false) { + public static function error ($code = 404, $logs = array (), $redirect = true) { $logs = self::processLogs ($logs); $error_filename = APP_PATH . '/Controllers/errorController.php'; - switch ($code) { - case 200 : - header('HTTP/1.1 200 OK'); - break; - case 403 : - header('HTTP/1.1 403 Forbidden'); - break; - case 404 : - header('HTTP/1.1 404 Not Found'); - break; - case 500 : - header('HTTP/1.1 500 Internal Server Error'); - break; - case 503 : - header('HTTP/1.1 503 Service Unavailable'); - break; - default : - header('HTTP/1.1 500 Internal Server Error'); - } - if (file_exists ($error_filename)) { - $params = array ( - 'code' => $code, - 'logs' => $logs - ); + Minz_Session::_param('error_code', $code); + Minz_Session::_param('error_logs', $logs); - if ($redirect) { - Minz_Request::forward (array ( - 'c' => 'error' - ), true); - } else { - Minz_Request::forward (array ( - 'c' => 'error', - 'params' => $params - ), false); - } + Minz_Request::forward (array ( + 'c' => 'error' + ), $redirect); } else { echo '

      An error occured

      ' . "\n"; @@ -82,7 +53,8 @@ class Minz_Error { * > en fonction de l'environment */ private static function processLogs ($logs) { - $env = Minz_Configuration::environment (); + $conf = Minz_Configuration::get('system'); + $env = $conf->environment; $logs_ok = array (); $error = array (); $warning = array (); @@ -98,10 +70,10 @@ class Minz_Error { $notice = $logs['notice']; } - if ($env == Minz_Configuration::PRODUCTION) { + if ($env == 'production') { $logs_ok = $error; } - if ($env == Minz_Configuration::DEVELOPMENT) { + if ($env == 'development') { $logs_ok = array_merge ($error, $warning, $notice); } diff --git a/sources/lib/Minz/Extension.php b/sources/lib/Minz/Extension.php new file mode 100755 index 0000000..d7ee8fe --- /dev/null +++ b/sources/lib/Minz/Extension.php @@ -0,0 +1,208 @@ +name = $meta_info['name']; + $this->entrypoint = $meta_info['entrypoint']; + $this->path = $meta_info['path']; + $this->author = isset($meta_info['author']) ? $meta_info['author'] : ''; + $this->description = isset($meta_info['description']) ? $meta_info['description'] : ''; + $this->version = isset($meta_info['version']) ? $meta_info['version'] : '0.1'; + $this->setType(isset($meta_info['type']) ? $meta_info['type'] : 'user'); + + $this->is_enabled = false; + } + + /** + * Used when installing an extension (e.g. update the database scheme). + * + * It must be redefined by child classes. + * + * @return true if the extension has been installed or a string explaining + * the problem. + */ + public function install() { + return true; + } + + /** + * Used when uninstalling an extension (e.g. revert the database scheme to + * cancel changes from install). + * + * It must be redefined by child classes. + * + * @return true if the extension has been uninstalled or a string explaining + * the problem. + */ + public function uninstall() { + return true; + } + + /** + * Call at the initialization of the extension (i.e. when the extension is + * enabled by the extension manager). + * + * It must be redefined by child classes. + */ + public function init() {} + + /** + * Set the current extension to enable. + */ + public function enable() { + $this->is_enabled = true; + } + + /** + * Return if the extension is currently enabled. + * + * @return true if extension is enabled, false else. + */ + public function isEnabled() { + return $this->is_enabled; + } + + /** + * Return the content of the configure view for the current extension. + * + * @return the html content from ext_dir/configure.phtml, false if it does + * not exist. + */ + public function getConfigureView() { + $filename = $this->path . '/configure.phtml'; + if (!file_exists($filename)) { + return false; + } + + ob_start(); + include($filename); + return ob_get_clean(); + } + + /** + * Handle the configure action. + * + * It must be redefined by child classes. + */ + public function handleConfigureAction() {} + + /** + * Getters and setters. + */ + public function getName() { + return $this->name; + } + public function getEntrypoint() { + return $this->entrypoint; + } + public function getPath() { + return $this->path; + } + public function getAuthor() { + return $this->author; + } + public function getDescription() { + return $this->description; + } + public function getVersion() { + return $this->version; + } + public function getType() { + return $this->type; + } + private function setType($type) { + if (!in_array($type, self::$authorized_types)) { + throw new Minz_ExtensionException('invalid `type` info', $this->name); + } + $this->type = $type; + } + + /** + * Return the url for a given file. + * + * @param $filename name of the file to serve. + * @param $type the type (js or css) of the file to serve. + * @return the url corresponding to the file. + */ + public function getFileUrl($filename, $type) { + $dir = substr(strrchr($this->path, '/'), 1); + $file_name_url = urlencode($dir . '/static/' . $filename); + + $absolute_path = $this->path . '/static/' . $filename; + $mtime = @filemtime($absolute_path); + + $url = '/ext.php?f=' . $file_name_url . + '&t=' . $type . + '&' . $mtime; + return Minz_Url::display($url); + } + + /** + * Register a controller in the Dispatcher. + * + * @param @base_name the base name of the controller. Final name will be: + * FreshExtension__Controller. + */ + public function registerController($base_name) { + Minz_Dispatcher::registerController($base_name, $this->path); + } + + /** + * Register the views in order to be accessible by the application. + */ + public function registerViews() { + Minz_View::addBasePathname($this->path); + } + + /** + * Register i18n files from ext_dir/i18n/ + */ + public function registerTranslates() { + $i18n_dir = $this->path . '/i18n'; + Minz_Translate::registerPath($i18n_dir); + } + + /** + * Register a new hook. + * + * @param $hook_name the hook name (must exist). + * @param $hook_function the function name to call (must be callable). + */ + public function registerHook($hook_name, $hook_function) { + Minz_ExtensionManager::addHook($hook_name, $hook_function, $this); + } +} diff --git a/sources/lib/Minz/ExtensionException.php b/sources/lib/Minz/ExtensionException.php new file mode 100755 index 0000000..647f1a9 --- /dev/null +++ b/sources/lib/Minz/ExtensionException.php @@ -0,0 +1,15 @@ + array( // function($entry) -> Entry | null + 'list' => array(), + 'signature' => 'OneToOne', + ), + 'entry_before_insert' => array( // function($entry) -> Entry | null + 'list' => array(), + 'signature' => 'OneToOne', + ), + 'feed_before_insert' => array( // function($feed) -> Feed | null + 'list' => array(), + 'signature' => 'OneToOne', + ), + 'post_update' => array( // function(none) -> none + 'list' => array(), + 'signature' => 'NoneToNone', + ), + ); + private static $ext_to_hooks = array(); + + /** + * Initialize the extension manager by loading extensions in EXTENSIONS_PATH. + * + * A valid extension is a directory containing metadata.json and + * extension.php files. + * metadata.json is a JSON structure where the only required fields are + * `name` and `entry_point`. + * extension.php should contain at least a class named Extension where + * must match with the entry point in metadata.json. This class must + * inherit from Minz_Extension class. + */ + public static function init() { + $list_potential_extensions = array_values(array_diff( + scandir(EXTENSIONS_PATH), + array('..', '.') + )); + + $system_conf = Minz_Configuration::get('system'); + self::$ext_auto_enabled = $system_conf->extensions_enabled; + + foreach ($list_potential_extensions as $ext_dir) { + $ext_pathname = EXTENSIONS_PATH . '/' . $ext_dir; + if (!is_dir($ext_pathname)) { + continue; + } + $metadata_filename = $ext_pathname . '/' . self::$ext_metaname; + + // Try to load metadata file. + if (!file_exists($metadata_filename)) { + // No metadata file? Invalid! + continue; + } + $meta_raw_content = file_get_contents($metadata_filename); + $meta_json = json_decode($meta_raw_content, true); + if (!$meta_json || !self::isValidMetadata($meta_json)) { + // metadata.json is not a json file? Invalid! + // or metadata.json is invalid (no required information), invalid! + Minz_Log::warning('`' . $metadata_filename . '` is not a valid metadata file'); + continue; + } + + $meta_json['path'] = $ext_pathname; + + // Try to load extension itself + $extension = self::load($meta_json); + if (!is_null($extension)) { + self::register($extension); + } + } + } + + /** + * Indicates if the given parameter is a valid metadata array. + * + * Required fields are: + * - `name`: the name of the extension + * - `entry_point`: a class name to load the extension source code + * If the extension class name is `TestExtension`, entry point will be `Test`. + * `entry_point` must be composed of alphanumeric characters. + * + * @param $meta is an array of values. + * @return true if the array is valid, false else. + */ + public static function isValidMetadata($meta) { + $valid_chars = array('_'); + return !(empty($meta['name']) || + empty($meta['entrypoint']) || + !ctype_alnum(str_replace($valid_chars, '', $meta['entrypoint']))); + } + + /** + * Load the extension source code based on info metadata. + * + * @param $info an array containing information about extension. + * @return an extension inheriting from Minz_Extension. + */ + public static function load($info) { + $entry_point_filename = $info['path'] . '/' . self::$ext_entry_point; + $ext_class_name = $info['entrypoint'] . 'Extension'; + + include_once($entry_point_filename); + + // Test if the given extension class exists. + if (!class_exists($ext_class_name)) { + Minz_Log::warning('`' . $ext_class_name . + '` cannot be found in `' . $entry_point_filename . '`'); + return null; + } + + // Try to load the class. + $extension = null; + try { + $extension = new $ext_class_name($info); + } catch (Minz_ExtensionException $e) { + // We cannot load the extension? Invalid! + Minz_Log::warning('In `' . $metadata_filename . '`: ' . $e->getMessage()); + return null; + } + + // Test if class is correct. + if (!($extension instanceof Minz_Extension)) { + Minz_Log::warning('`' . $ext_class_name . + '` is not an instance of `Minz_Extension`'); + return null; + } + + return $extension; + } + + /** + * Add the extension to the list of the known extensions ($ext_list). + * + * If the extension is present in $ext_auto_enabled and if its type is "system", + * it will be enabled in the same time. + * + * @param $ext a valid extension. + */ + public static function register($ext) { + $name = $ext->getName(); + self::$ext_list[$name] = $ext; + + if ($ext->getType() === 'system' && + in_array($name, self::$ext_auto_enabled)) { + self::enable($ext->getName()); + } + + self::$ext_to_hooks[$name] = array(); + } + + /** + * Enable an extension so it will be called when necessary. + * + * The extension init() method will be called. + * + * @param $ext_name is the name of a valid extension present in $ext_list. + */ + public static function enable($ext_name) { + if (isset(self::$ext_list[$ext_name])) { + $ext = self::$ext_list[$ext_name]; + self::$ext_list_enabled[$ext_name] = $ext; + $ext->enable(); + $ext->init(); + } + } + + /** + * Enable a list of extensions. + * + * @param $ext_list the names of extensions we want to load. + */ + public static function enableByList($ext_list) { + foreach ($ext_list as $ext_name) { + self::enable($ext_name); + } + } + + /** + * Return a list of extensions. + * + * @param $only_enabled if true returns only the enabled extensions (false by default). + * @return an array of extensions. + */ + public static function listExtensions($only_enabled = false) { + if ($only_enabled) { + return self::$ext_list_enabled; + } else { + return self::$ext_list; + } + } + + /** + * Return an extension by its name. + * + * @param $ext_name the name of the extension. + * @return the corresponding extension or null if it doesn't exist. + */ + public static function findExtension($ext_name) { + if (!isset(self::$ext_list[$ext_name])) { + return null; + } + + return self::$ext_list[$ext_name]; + } + + /** + * Add a hook function to a given hook. + * + * The hook name must be a valid one. For the valid list, see self::$hook_list + * array keys. + * + * @param $hook_name the hook name (must exist). + * @param $hook_function the function name to call (must be callable). + * @param $ext the extension which register the hook. + */ + public static function addHook($hook_name, $hook_function, $ext) { + if (isset(self::$hook_list[$hook_name]) && is_callable($hook_function)) { + self::$hook_list[$hook_name]['list'][] = $hook_function; + self::$ext_to_hooks[$ext->getName()][] = $hook_name; + } + } + + /** + * Call functions related to a given hook. + * + * The hook name must be a valid one. For the valid list, see self::$hook_list + * array keys. + * + * @param $hook_name the hook to call. + * @param additionnal parameters (for signature, please see self::$hook_list). + * @return the final result of the called hook. + */ + public static function callHook($hook_name) { + if (!isset(self::$hook_list[$hook_name])) { + return; + } + + $signature = self::$hook_list[$hook_name]['signature']; + $signature = 'self::call' . $signature; + $args = func_get_args(); + + return call_user_func_array($signature, $args); + } + + /** + * Call a hook which takes one argument and return a result. + * + * The result is chained between the extension, for instance, first extension + * hook will receive the initial argument and return a result which will be + * passed as an argument to the next extension hook and so on. + * + * If a hook return a null value, the method is stopped and return null. + * + * @param $hook_name is the hook to call. + * @param $arg is the argument to pass to the first extension hook. + * @return the final chained result of the hooks. If nothing is changed, + * the initial argument is returned. + */ + private static function callOneToOne($hook_name, $arg) { + $result = $arg; + foreach (self::$hook_list[$hook_name]['list'] as $function) { + $result = call_user_func($function, $arg); + + if (is_null($result)) { + break; + } + + $arg = $result; + } + return $result; + } + + /** + * Call a hook which takes no argument and returns nothing. + * + * This case is simpler than callOneToOne because hooks are called one by + * one, without any consideration of argument nor result. + * + * @param $hook_name is the hook to call. + */ + private static function callNoneToNone($hook_name) { + foreach (self::$hook_list[$hook_name]['list'] as $function) { + call_user_func($function); + } + } +} diff --git a/sources/lib/Minz/FrontController.php b/sources/lib/Minz/FrontController.php index f138828..f9eff3d 100755 --- a/sources/lib/Minz/FrontController.php +++ b/sources/lib/Minz/FrontController.php @@ -30,14 +30,13 @@ class Minz_FrontController { * Initialise le dispatcher, met à jour la Request */ public function __construct () { - if (LOG_PATH === false) { - $this->killApp ('Path not found: LOG_PATH'); - } - try { - Minz_Configuration::init (); + Minz_Configuration::register('system', + DATA_PATH . '/config.php', + DATA_PATH . '/config.default.php'); + $this->setReporting(); - Minz_Request::init (); + Minz_Request::init(); $url = $this->buildUrl(); $url['params'] = array_merge ( @@ -46,7 +45,7 @@ class Minz_FrontController { ); Minz_Request::forward ($url); } catch (Minz_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::ERROR); + Minz_Log::error($e->getMessage()); $this->killApp ($e->getMessage ()); } @@ -85,7 +84,7 @@ class Minz_FrontController { $this->dispatcher->run(); } catch (Minz_Exception $e) { try { - Minz_Log::record ($e->getMessage (), Minz_Log::ERROR); + Minz_Log::error($e->getMessage()); } catch (Minz_PermissionDeniedException $e) { $this->killApp ($e->getMessage ()); } @@ -114,4 +113,23 @@ class Minz_FrontController { } exit ('### Application problem ###
      '."\n".$txt); } + + private function setReporting() { + $conf = Minz_Configuration::get('system'); + switch($conf->environment) { + case 'production': + error_reporting(E_ALL); + ini_set('display_errors','Off'); + ini_set('log_errors', 'On'); + break; + case 'development': + error_reporting(E_ALL); + ini_set('display_errors','On'); + ini_set('log_errors', 'On'); + break; + case 'silent': + error_reporting(0); + break; + } + } } diff --git a/sources/lib/Minz/Log.php b/sources/lib/Minz/Log.php index d3eaec2..2a9e109 100755 --- a/sources/lib/Minz/Log.php +++ b/sources/lib/Minz/Log.php @@ -28,16 +28,21 @@ class Minz_Log { * - level = NOTICE et environment = PRODUCTION * @param $information message d'erreur / information à enregistrer * @param $level niveau d'erreur - * @param $file_name fichier de log, par défaut LOG_PATH/application.log + * @param $file_name fichier de log */ public static function record ($information, $level, $file_name = null) { - $env = Minz_Configuration::environment (); + try { + $conf = Minz_Configuration::get('system'); + $env = $conf->environment; + } catch (Minz_ConfigurationException $e) { + $env = 'production'; + } - if (! ($env === Minz_Configuration::SILENT - || ($env === Minz_Configuration::PRODUCTION + if (! ($env === 'silent' + || ($env === 'production' && ($level >= Minz_Log::NOTICE)))) { if ($file_name === null) { - $file_name = LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log'; + $file_name = join_path(USERS_PATH, Minz_Session::param('currentUser', '_'), 'log.txt'); } switch ($level) { @@ -71,7 +76,7 @@ class Minz_Log { * Automatise le log des variables globales $_GET et $_POST * Fait appel à la fonction record(...) * Ne fonctionne qu'en environnement "development" - * @param $file_name fichier de log, par défaut LOG_PATH/application.log + * @param $file_name fichier de log */ public static function recordRequest($file_name = null) { $msg_get = str_replace("\n", '', '$_GET content : ' . print_r($_GET, true)); diff --git a/sources/lib/Minz/ModelPdo.php b/sources/lib/Minz/ModelPdo.php index b4bfca7..ac7a1be 100755 --- a/sources/lib/Minz/ModelPdo.php +++ b/sources/lib/Minz/ModelPdo.php @@ -16,6 +16,8 @@ class Minz_ModelPdo { public static $useSharedBd = true; private static $sharedBd = null; private static $sharedPrefix; + private static $has_transaction = false; + private static $sharedCurrentUser; protected static $sharedDbType; /** @@ -23,6 +25,7 @@ class Minz_ModelPdo { */ protected $bd; + protected $current_user; protected $prefix; public function dbType() { @@ -37,14 +40,18 @@ class Minz_ModelPdo { if (self::$useSharedBd && self::$sharedBd != null && $currentUser === null) { $this->bd = self::$sharedBd; $this->prefix = self::$sharedPrefix; + $this->current_user = self::$sharedCurrentUser; return; } - $db = Minz_Configuration::dataBase(); + $conf = Minz_Configuration::get('system'); + $db = $conf->db; if ($currentUser === null) { $currentUser = Minz_Session::param('currentUser', '_'); } + $this->current_user = $currentUser; + self::$sharedCurrentUser = $currentUser; try { $type = $db['type']; @@ -57,7 +64,7 @@ class Minz_ModelPdo { ); $this->prefix = $db['prefix'] . $currentUser . '_'; } elseif ($type === 'sqlite') { - $string = 'sqlite:' . DATA_PATH . '/' . $currentUser . '.sqlite'; + $string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite'); $driver_options = array( //PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ); @@ -91,12 +98,18 @@ class Minz_ModelPdo { public function beginTransaction() { $this->bd->beginTransaction(); + self::$has_transaction = true; + } + public function hasTransaction() { + return self::$has_transaction; } public function commit() { $this->bd->commit(); + self::$has_transaction = false; } public function rollBack() { $this->bd->rollBack(); + self::$has_transaction = false; } public static function clean() { diff --git a/sources/lib/Minz/Request.php b/sources/lib/Minz/Request.php index f7a24c0..6db2e9c 100755 --- a/sources/lib/Minz/Request.php +++ b/sources/lib/Minz/Request.php @@ -45,6 +45,13 @@ class Minz_Request { public static function defaultActionName() { return self::$default_action_name; } + public static function currentRequest() { + return array( + 'c' => self::$controller_name, + 'a' => self::$action_name, + 'params' => self::$params, + ); + } /** * Setteurs @@ -89,7 +96,8 @@ class Minz_Request { * @return la base de l'url */ public static function getBaseUrl() { - $defaultBaseUrl = Minz_Configuration::baseUrl(); + $conf = Minz_Configuration::get('system'); + $defaultBaseUrl = $conf->base_url; if (!empty($defaultBaseUrl)) { return $defaultBaseUrl; } elseif (isset($_SERVER['REQUEST_URI'])) { diff --git a/sources/lib/Minz/Session.php b/sources/lib/Minz/Session.php index af4de75..058685a 100755 --- a/sources/lib/Minz/Session.php +++ b/sources/lib/Minz/Session.php @@ -55,7 +55,7 @@ class Minz_Session { if (!$force) { self::_param('language', $language); - Minz_Translate::reset(); + Minz_Translate::reset($language); } } @@ -65,7 +65,9 @@ class Minz_Session { * @param $l la durée de vie */ public static function keepCookie($l) { - $cookie_dir = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI']; + // Get the script_name (e.g. /p/i/index.php) and keep only the path. + $cookie_dir = empty($_SERVER['SCRIPT_NAME']) ? '' : $_SERVER['SCRIPT_NAME']; + $cookie_dir = dirname($cookie_dir); session_set_cookie_params($l, $cookie_dir, '', false, true); } diff --git a/sources/lib/Minz/Translate.php b/sources/lib/Minz/Translate.php index 8c2f900..baddcb4 100755 --- a/sources/lib/Minz/Translate.php +++ b/sources/lib/Minz/Translate.php @@ -5,71 +5,222 @@ */ /** - * La classe Translate se charge de la traduction - * Utilise les fichiers du répertoire /app/i18n/ + * This class is used for the internationalization. + * It uses files in `./app/i18n/` */ class Minz_Translate { /** - * $language est la langue à afficher + * $path_list is the list of registered base path to search translations. */ - private static $language; - + private static $path_list = array(); + /** - * $translates est le tableau de correspondance - * $key => $traduction + * $lang_name is the name of the current language to use. + */ + private static $lang_name; + + /** + * $lang_files is a list of registered i18n files. + */ + private static $lang_files = array(); + + /** + * $translates is a cache for i18n translation. */ private static $translates = array(); - + /** - * Inclus le fichier de langue qui va bien - * l'enregistre dans $translates + * Init the translation object. + * @param $lang_name the lang to show. */ - public static function init() { - $l = Minz_Configuration::language(); - self::$language = Minz_Session::param('language', $l); - - $l_path = APP_PATH . '/i18n/' . self::$language . '.php'; - - if (file_exists($l_path)) { - self::$translates = include($l_path); + public static function init($lang_name = null) { + self::$lang_name = $lang_name; + self::$lang_files = array(); + self::$translates = array(); + self::registerPath(APP_PATH . '/i18n'); + foreach (self::$path_list as $path) { + self::loadLang($path); } } - + /** - * Alias de init + * Reset the translation object with a new language. + * @param $lang_name the new language to use */ - public static function reset() { - self::init(); + public static function reset($lang_name) { + self::$lang_name = $lang_name; + self::$lang_files = array(); + self::$translates = array(); + foreach (self::$path_list as $path) { + self::loadLang($path); + } } - + /** - * Traduit une clé en sa valeur du tableau $translates - * @param $key la clé à traduire - * @return la valeur correspondante à la clé - * > si non présente dans le tableau, on retourne la clé elle-même - */ - public static function t($key) { - $translate = $key; - - if (isset(self::$translates[$key])) { - $translate = self::$translates[$key]; + * Return the list of available languages. + * @return an array containing langs found in different registered paths. + */ + public static function availableLanguages() { + $list_langs = array(); + + foreach (self::$path_list as $path) { + $path_langs = array_values(array_diff( + scandir($path), + array('..', '.') + )); + + $list_langs = array_merge($list_langs, $path_langs); } + return array_unique($list_langs); + } + + /** + * Register a new path. + * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). + */ + public static function registerPath($path) { + if (in_array($path, self::$path_list)) { + return; + } + + self::$path_list[] = $path; + self::loadLang($path); + } + + /** + * Load translations of the current language from the given path. + * @param $path the path containing i18n directories. + */ + private static function loadLang($path) { + $lang_path = $path . '/' . self::$lang_name; + if (!file_exists($lang_path) || is_null(self::$lang_name)) { + // The lang path does not exist, nothing more to do. + return; + } + + $list_i18n_files = array_values(array_diff( + scandir($lang_path), + array('..', '.') + )); + + // Each file basename correspond to a top-level i18n key. For each of + // these keys we store the file pathname and mark translations must be + // reloaded (by setting $translates[$i18n_key] to null). + foreach ($list_i18n_files as $i18n_filename) { + $i18n_key = basename($i18n_filename, '.php'); + if (!isset(self::$lang_files[$i18n_key])) { + self::$lang_files[$i18n_key] = array(); + } + self::$lang_files[$i18n_key][] = $lang_path . '/' . $i18n_filename; + self::$translates[$i18n_key] = null; + } + } + + /** + * Load the files associated to $key into $translates. + * @param $key the top level i18n key we want to load. + */ + private static function loadKey($key) { + // The top level key is not in $lang_files, it means it does not exist! + if (!isset(self::$lang_files[$key])) { + Minz_Log::debug($key . ' is not a valid top level key'); + return false; + } + + self::$translates[$key] = array(); + + foreach (self::$lang_files[$key] as $lang_pathname) { + $i18n_array = include($lang_pathname); + if (!is_array($i18n_array)) { + Minz_Log::warning('`' . $lang_pathname . '` does not contain a PHP array'); + continue; + } + + // We must avoid to erase previous data so we just override them if + // needed. + self::$translates[$key] = array_replace_recursive( + self::$translates[$key], $i18n_array + ); + } + + return true; + } + + /** + * Translate a key into its corresponding value based on selected language. + * @param $key the key to translate. + * @param additional parameters for variable keys. + * @return the value corresponding to the key. + * If no value is found, return the key itself. + */ + public static function t($key) { + $group = explode('.', $key); + + if (count($group) < 2) { + Minz_Log::debug($key . ' is not in a valid format'); + $top_level = 'gen'; + } else { + $top_level = array_shift($group); + } + + // If $translates[$top_level] is null it means we have to load the + // corresponding files. + if (!isset(self::$translates[$top_level]) || + is_null(self::$translates[$top_level])) { + $res = self::loadKey($top_level); + if (!$res) { + return $key; + } + } + + // Go through the i18n keys to get the correct translation value. + $translates = self::$translates[$top_level]; + $size_group = count($group); + $level_processed = 0; + $translation_value = $key; + foreach ($group as $i18n_level) { + $level_processed++; + if (!isset($translates[$i18n_level])) { + Minz_Log::debug($key . ' is not a valid key'); + return $key; + } + + if ($level_processed < $size_group) { + $translates = $translates[$i18n_level]; + } else { + $translation_value = $translates[$i18n_level]; + } + } + + if (is_array($translation_value)) { + if (isset($translation_value['_'])) { + $translation_value = $translation_value['_']; + } else { + Minz_Log::debug($key . ' is not a valid key'); + return $key; + } + } + + // Get the facultative arguments to replace i18n variables. $args = func_get_args(); unset($args[0]); - - return vsprintf($translate, $args); + + return vsprintf($translation_value, $args); } - + /** - * Retourne la langue utilisée actuellement - * @return la langue + * Return the current language. */ public static function language() { - return self::$language; + return self::$lang_name; } } + +/** + * Alias for Minz_Translate::t() + */ function _t($key) { $args = func_get_args(); unset($args[0]); diff --git a/sources/lib/Minz/Url.php b/sources/lib/Minz/Url.php index e9f9a69..af555a2 100755 --- a/sources/lib/Minz/Url.php +++ b/sources/lib/Minz/Url.php @@ -45,45 +45,45 @@ class Minz_Url { return $url_string; } - + /** * Construit l'URI d'une URL * @param l'url sous forme de tableau * @param $encodage pour indiquer comment encoder les & (& ou & pour html) * @return l'uri sous la forme ?key=value&key2=value2 */ - private static function printUri ($url, $encodage) { + private static function printUri($url, $encodage) { $uri = ''; $separator = '?'; - - if($encodage == 'html') { + + if ($encodage === 'html') { $and = '&'; } else { $and = '&'; } - - if (isset ($url['c']) - && $url['c'] != Minz_Request::defaultControllerName ()) { + + if (isset($url['c']) + && $url['c'] != Minz_Request::defaultControllerName()) { $uri .= $separator . 'c=' . $url['c']; $separator = $and; } - - if (isset ($url['a']) - && $url['a'] != Minz_Request::defaultActionName ()) { + + if (isset($url['a']) + && $url['a'] != Minz_Request::defaultActionName()) { $uri .= $separator . 'a=' . $url['a']; $separator = $and; } - - if (isset ($url['params'])) { + + if (isset($url['params'])) { foreach ($url['params'] as $key => $param) { - $uri .= $separator . $key . '=' . $param; + $uri .= $separator . urlencode($key) . '=' . urlencode($param); $separator = $and; } } - + return $uri; } - + /** * Vérifie que les éléments du tableau représentant une url soit ok * @param l'url sous forme de tableau (sinon renverra directement $url) @@ -91,7 +91,7 @@ class Minz_Url { */ public static function checkUrl ($url) { $url_checked = $url; - + if (is_array ($url)) { if (!isset ($url['c'])) { $url_checked['c'] = Minz_Request::defaultControllerName (); @@ -103,7 +103,7 @@ class Minz_Url { $url_checked['params'] = array (); } } - + return $url_checked; } } diff --git a/sources/lib/Minz/View.php b/sources/lib/Minz/View.php index a0dec18..ff5cce4 100755 --- a/sources/lib/Minz/View.php +++ b/sources/lib/Minz/View.php @@ -13,8 +13,9 @@ class Minz_View { const LAYOUT_FILENAME = '/layout.phtml'; private $view_filename = ''; - private $use_layout = null; + private $use_layout = true; + private static $base_pathnames = array(APP_PATH); private static $title = ''; private static $styles = array (); private static $scripts = array (); @@ -28,26 +29,35 @@ class Minz_View { public function __construct () { $this->change_view(Minz_Request::controllerName(), Minz_Request::actionName()); - self::$title = Minz_Configuration::title (); + + $conf = Minz_Configuration::get('system'); + self::$title = $conf->title; } /** * Change le fichier de vue en fonction d'un controller / action */ public function change_view($controller_name, $action_name) { - $this->view_filename = APP_PATH - . self::VIEWS_PATH_NAME . '/' + $this->view_filename = self::VIEWS_PATH_NAME . '/' . $controller_name . '/' . $action_name . '.phtml'; } + /** + * Add a base pathname to search views. + * + * New pathnames will be added at the beginning of the list. + * + * @param $base_pathname the new base pathname. + */ + public static function addBasePathname($base_pathname) { + array_unshift(self::$base_pathnames, $base_pathname); + } + /** * Construit la vue */ public function build () { - if ($this->use_layout === null) { //TODO: avoid file_exists and require views to be explicit - $this->use_layout = file_exists (APP_PATH . self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME); - } if ($this->use_layout) { $this->buildLayout (); } else { @@ -55,25 +65,41 @@ class Minz_View { } } + /** + * Include a view file. + * + * The file is searched inside list of $base_pathnames. + * + * @param $filename the name of the file to include. + * @return true if the file has been included, false else. + */ + private function includeFile($filename) { + // We search the filename in the list of base pathnames. Only the first view + // found is considered. + foreach (self::$base_pathnames as $base) { + $absolute_filename = $base . $filename; + if (file_exists($absolute_filename)) { + include $absolute_filename; + return true; + } + } + + return false; + } + /** * Construit le layout */ public function buildLayout () { - include ( - APP_PATH - . self::LAYOUT_PATH_NAME - . self::LAYOUT_FILENAME - ); + $this->includeFile(self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME); } /** * Affiche la Vue en elle-même */ public function render () { - if ((include($this->view_filename)) === false) { - Minz_Log::record ('File not found: `' - . $this->view_filename . '`', - Minz_Log::NOTICE); + if (!$this->includeFile($this->view_filename)) { + Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } @@ -82,14 +108,9 @@ class Minz_View { * @param $part l'élément partial à ajouter */ public function partial ($part) { - $fic_partial = APP_PATH - . self::LAYOUT_PATH_NAME . '/' - . $part . '.phtml'; - - if ((include($fic_partial)) === false) { - Minz_Log::record ('File not found: `' - . $fic_partial . '`', - Minz_Log::WARNING); + $fic_partial = self::LAYOUT_PATH_NAME . '/' . $part . '.phtml'; + if (!$this->includeFile($fic_partial)) { + Minz_Log::warning('File not found: `' . $fic_partial . '`'); } } @@ -98,14 +119,9 @@ class Minz_View { * @param $helper l'élément à afficher */ public function renderHelper ($helper) { - $fic_helper = APP_PATH - . '/views/helpers/' - . $helper . '.phtml'; - - if ((include($fic_helper)) === false) {; - Minz_Log::record ('File not found: `' - . $fic_helper . '`', - Minz_Log::WARNING); + $fic_helper = '/views/helpers/' . $helper . '.phtml'; + if (!$this->includeFile($fic_helper)) { + Minz_Log::warning('File not found: `' . $fic_helper . '`'); } } diff --git a/sources/lib/SimplePie/SimplePie.php b/sources/lib/SimplePie/SimplePie.php index 06c100f..c4872b5 100755 --- a/sources/lib/SimplePie/SimplePie.php +++ b/sources/lib/SimplePie/SimplePie.php @@ -1455,7 +1455,11 @@ class SimplePie { // Load the Cache $this->data = $cache->load(); - if (!empty($this->data)) + if ($cache->mtime() + $this->cache_duration > time()) { //FreshRSS + $this->raw_data = false; + return true; // If the cache is still valid, just return true + } + elseif (!empty($this->data)) { // If the cache is for an outdated build of SimplePie if (!isset($this->data['build']) || $this->data['build'] !== SIMPLEPIE_BUILD) @@ -1487,7 +1491,7 @@ class SimplePie } } // Check if the cache has been updated - elseif ($cache->mtime() + $this->cache_duration < time()) + else //if ($cache->mtime() + $this->cache_duration < time()) //FreshRSS removed { // If we have last-modified and/or etag set //if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag'])) //FreshRSS removed @@ -1516,6 +1520,7 @@ class SimplePie } else { + $cache->touch(); //FreshRSS $this->error = $file->error; //FreshRSS return !empty($this->data); //FreshRSS //unset($file); //FreshRSS removed @@ -1524,26 +1529,27 @@ class SimplePie { //FreshRSS $md5 = $this->cleanMd5($file->body); if ($this->data['md5'] === $md5) { - syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url); + // syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url); $cache->touch(); return true; //Content unchanged even though server did not send a 304 } else { - syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url); + // syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url); $this->data['md5'] = $md5; } } } - // If the cache is still valid, just return true - else - { - $this->raw_data = false; - return true; - } + //// If the cache is still valid, just return true + //else //FreshRSS removed + //{ + // $this->raw_data = false; + // return true; + //} } // If the cache is empty, delete it else { - $cache->unlink(); + //$cache->unlink(); //FreshRSS removed + $cache->touch(); //FreshRSS $this->data = array(); } } @@ -1576,13 +1582,15 @@ class SimplePie if (!$locate->is_feed($file)) { + $copyStatusCode = $file->status_code; //FreshRSS + $copyContentType = $file->headers['content-type']; //FreshRSS // We need to unset this so that if SimplePie::set_file() has been called that object is untouched unset($file); try { if (!($file = $locate->find($this->autodiscovery, $this->all_discovered_feeds))) { - $this->error = "A feed could not be found at $this->feed_url. A feed with an invalid mime type may fall victim to this error, or " . SIMPLEPIE_NAME . " was unable to auto-discover it.. Use force_feed() if you are certain this URL is a real feed."; + $this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`"; //FreshRSS $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); return false; } diff --git a/sources/lib/SimplePie/SimplePie/Cache/File.php b/sources/lib/SimplePie/SimplePie/Cache/File.php index 3b16354..cb4b528 100755 --- a/sources/lib/SimplePie/SimplePie/Cache/File.php +++ b/sources/lib/SimplePie/SimplePie/Cache/File.php @@ -136,11 +136,11 @@ class SimplePie_Cache_File implements SimplePie_Cache_Base */ public function mtime() { - if (file_exists($this->name)) + //if (file_exists($this->name)) //FreshRSS removed { - return filemtime($this->name); + return @filemtime($this->name); //FreshRSS } - return false; + //return false; //FreshRSS removed } /** @@ -150,11 +150,11 @@ class SimplePie_Cache_File implements SimplePie_Cache_Base */ public function touch() { - if (file_exists($this->name)) + //if (file_exists($this->name)) //FreshRSS removed { - return touch($this->name); + return @touch($this->name); //FreshRSS } - return false; + //return false; //FreshRSS removed } /** diff --git a/sources/lib/SimplePie/SimplePie/File.php b/sources/lib/SimplePie/SimplePie/File.php index b1bbe44..9625af2 100755 --- a/sources/lib/SimplePie/SimplePie/File.php +++ b/sources/lib/SimplePie/SimplePie/File.php @@ -79,7 +79,7 @@ class SimplePie_File $this->useragent = $useragent; if (preg_match('/^http(s)?:\/\//i', $url)) { - syslog(LOG_INFO, 'SimplePie GET ' . $url); //FreshRSS + // syslog(LOG_INFO, 'SimplePie GET ' . $url); //FreshRSS if ($useragent === null) { $useragent = ini_get('user_agent'); diff --git a/sources/lib/SimplePie/SimplePie/Locator.php b/sources/lib/SimplePie/SimplePie/Locator.php index 90ee7a3..4e5f7c1 100755 --- a/sources/lib/SimplePie/SimplePie/Locator.php +++ b/sources/lib/SimplePie/SimplePie/Locator.php @@ -148,7 +148,7 @@ class SimplePie_Locator { $sniffer = $this->registry->create('Content_Type_Sniffer', array($file)); $sniffed = $sniffer->get_type(); - if (in_array($sniffed, array('application/rss+xml', 'application/rdf+xml', 'text/rdf', 'application/atom+xml', 'text/xml', 'application/xml'))) + if (in_array($sniffed, array('application/rss+xml', 'application/rdf+xml', 'text/rdf', 'application/atom+xml', 'text/xml', 'application/xml', 'application/x-rss+xml'))) //FreshRSS { return true; } diff --git a/sources/lib/SimplePie/SimplePie/Parse/Date.php b/sources/lib/SimplePie/SimplePie/Parse/Date.php index ef800f1..ba7c070 100755 --- a/sources/lib/SimplePie/SimplePie/Parse/Date.php +++ b/sources/lib/SimplePie/SimplePie/Parse/Date.php @@ -331,6 +331,7 @@ class SimplePie_Parse_Date 'CCT' => 23400, 'CDT' => -18000, 'CEDT' => 7200, + 'CEST' => 7200, //FreshRSS 'CET' => 3600, 'CGST' => -7200, 'CGT' => -10800, diff --git a/sources/lib/lib_opml.php b/sources/lib/lib_opml.php index 16a9921..02ae5f5 100755 --- a/sources/lib/lib_opml.php +++ b/sources/lib/lib_opml.php @@ -1,11 +1,19 @@ + * @link https://github.com/marienfressinaud/lib_opml + * @version 0.2 + * @license public domain * * Usages: * > include('lib_opml.php'); @@ -23,21 +31,44 @@ * > echo $opml_string; * > print_r($opml_object); * + * You can set $strict argument to false if you want to bypass "text" attribute + * requirement. + * * If parsing fails for any reason (e.g. not an XML string, does not match with * the specifications), a LibOPML_Exception is raised. * - * Author: Marien Fressinaud - * Url: https://github.com/marienfressinaud/lib_opml - * Version: 0.1 - * Date: 2014-03-29 - * License: public domain + * lib_opml array format is described here: + * $array = array( + * 'head' => array( // 'head' element is optional (but recommended) + * 'key' => 'value', // key must be a part of available OPML head elements + * ), + * 'body' => array( // body is required + * array( // this array represents an outline (at least one) + * 'text' => 'value', // 'text' element is required if $strict is true + * 'key' => 'value', // key and value are what you want (optional) + * '@outlines' = array( // @outlines is a special value and represents sub-outlines + * array( + * [...] // where [...] is a valid outline definition + * ), + * ), + * ), + * array( // other outline definitions + * [...] + * ), + * [...], + * ) + * ) * - * */ + */ +/** + * A simple Exception class which represents any kind of OPML problem. + * Message should precise the current problem. + */ class LibOPML_Exception extends Exception {} -// These elements are optional +// Define the list of available head attributes. All of them are optional. define('HEAD_ELEMENTS', serialize(array( 'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail', 'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop', @@ -45,7 +76,16 @@ define('HEAD_ELEMENTS', serialize(array( ))); -function libopml_parse_outline($outline_xml) { +/** + * Parse an XML object as an outline object and return corresponding array + * + * @param SimpleXMLElement $outline_xml the XML object we want to parse + * @param bool $strict true if "text" attribute is required, false else + * @return array corresponding to an outline and following format described above + * @throws LibOPML_Exception + * @access private + */ +function libopml_parse_outline($outline_xml, $strict = true) { $outline = array(); // An outline may contain any kind of attributes but "text" attribute is @@ -59,7 +99,7 @@ function libopml_parse_outline($outline_xml) { } } - if (!$text_is_present) { + if (!$text_is_present && $strict) { throw new LibOPML_Exception( 'Outline does not contain any text attribute' ); @@ -68,7 +108,7 @@ function libopml_parse_outline($outline_xml) { foreach ($outline_xml->children() as $key => $value) { // An outline may contain any number of outline children if ($key === 'outline') { - $outline['@outlines'][] = libopml_parse_outline($value); + $outline['@outlines'][] = libopml_parse_outline($value, $strict); } else { throw new LibOPML_Exception( 'Body can contain only outline elements' @@ -80,7 +120,16 @@ function libopml_parse_outline($outline_xml) { } -function libopml_parse_string($xml) { +/** + * Parse a string as a XML one and returns the corresponding array + * + * @param string $xml is the string we want to parse + * @param bool $strict true if "text" attribute is required, false else + * @return array corresponding to the XML string and following format described above + * @throws LibOPML_Exception + * @access public + */ +function libopml_parse_string($xml, $strict = true) { $dom = new DOMDocument(); $dom->recover = true; $dom->strictErrorChecking = false; @@ -117,7 +166,7 @@ function libopml_parse_string($xml) { foreach ($opml->body->children() as $key => $value) { if ($key === 'outline') { $at_least_one_outline = true; - $array['body'][] = libopml_parse_outline($value); + $array['body'][] = libopml_parse_outline($value, $strict); } else { throw new LibOPML_Exception( 'Body can contain only outline elements' @@ -135,7 +184,16 @@ function libopml_parse_string($xml) { } -function libopml_parse_file($filename) { +/** + * Parse a string contained into a file as a XML string and returns the corresponding array + * + * @param string $filename should indicates a valid XML file + * @param bool $strict true if "text" attribute is required, false else + * @return array corresponding to the file content and following format described above + * @throws LibOPML_Exception + * @access public + */ +function libopml_parse_file($filename, $strict = true) { $file_content = file_get_contents($filename); if ($file_content === false) { @@ -144,11 +202,20 @@ function libopml_parse_file($filename) { ); } - return libopml_parse_string($file_content); + return libopml_parse_string($file_content, $strict); } -function libopml_render_outline($parent_elt, $outline) { +/** + * Create a XML outline object in a parent object. + * + * @param SimpleXMLElement $parent_elt is the parent object of current outline + * @param array $outline array representing an outline object + * @param bool $strict true if "text" attribute is required, false else + * @throws LibOPML_Exception + * @access private + */ +function libopml_render_outline($parent_elt, $outline, $strict) { // Outline MUST be an array! if (!is_array($outline)) { throw new LibOPML_Exception( @@ -163,7 +230,7 @@ function libopml_render_outline($parent_elt, $outline) { // outline elements. if ($key === '@outlines' && is_array($value)) { foreach ($value as $outline_child) { - libopml_render_outline($outline_elt, $outline_child); + libopml_render_outline($outline_elt, $outline_child, $strict); } } elseif (is_array($value)) { throw new LibOPML_Exception( @@ -179,7 +246,7 @@ function libopml_render_outline($parent_elt, $outline) { } } - if (!$text_is_present) { + if (!$text_is_present && $strict) { throw new LibOPML_Exception( 'You must define at least a text element for all outlines' ); @@ -187,8 +254,19 @@ function libopml_render_outline($parent_elt, $outline) { } -function libopml_render($array, $as_xml_object = false) { - $opml = new SimpleXMLElement(''); +/** + * Render an array as an OPML string or a XML object. + * + * @param array $array is the array we want to render and must follow structure defined above + * @param bool $as_xml_object false if function must return a string, true for a XML object + * @param bool $strict true if "text" attribute is required, false else + * @return string|SimpleXMLElement XML string corresponding to $array or XML object + * @throws LibOPML_Exception + * @access public + */ +function libopml_render($array, $as_xml_object = false, $strict = true) { + $opml = new SimpleXMLElement(''); + $opml->addAttribute('version', $strict ? '2.0' : '1.0'); // Create head element. $array['head'] is optional but head element will // exist in the final XML object. @@ -216,7 +294,7 @@ function libopml_render($array, $as_xml_object = false) { // Create outline elements $body = $opml->addChild('body'); foreach ($array['body'] as $outline) { - libopml_render_outline($body, $outline); + libopml_render_outline($body, $outline, $strict); } // And return the final result diff --git a/sources/lib/lib_rss.php b/sources/lib/lib_rss.php index 31c9cdb..e5fe730 100755 --- a/sources/lib/lib_rss.php +++ b/sources/lib/lib_rss.php @@ -15,6 +15,17 @@ if (!function_exists('json_encode')) { } } +/** + * Build a directory path by concatenating a list of directory names. + * + * @param $path_parts a list of directory names + * @return a string corresponding to the final pathname + */ +function join_path() { + $path_parts = func_get_args(); + return join(DIRECTORY_SEPARATOR, $path_parts); +} + // function classAutoloader($class) { if (strpos($class, 'FreshRSS') === 0) { @@ -56,12 +67,14 @@ function checkUrl($url) { } } -function formatNumber($n, $precision = 0) { - return str_replace(' ', ' ', //Espace insécable //TODO: remplacer par une espace _fine_ insécable - number_format($n, $precision, '.', ' ')); //number_format does not seem to be Unicode-compatible +function format_number($n, $precision = 0) { + // number_format does not seem to be Unicode-compatible + return str_replace(' ', ' ', //Espace fine insécable + number_format($n, $precision, '.', ' ') + ); } -function formatBytes($bytes, $precision = 2, $system = 'IEC') { +function format_bytes($bytes, $precision = 2, $system = 'IEC') { if ($system === 'IEC') { $base = 1024; $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB'); @@ -73,15 +86,15 @@ function formatBytes($bytes, $precision = 2, $system = 'IEC') { $pow = $bytes === 0 ? 0 : floor(log($bytes) / log($base)); $pow = min($pow, count($units) - 1); $bytes /= pow($base, $pow); - return formatNumber($bytes, $precision) . ' ' . $units[$pow]; + return format_number($bytes, $precision) . ' ' . $units[$pow]; } function timestamptodate ($t, $hour = true) { - $month = Minz_Translate::t (date('M', $t)); + $month = _t('gen.date.' . date('M', $t)); if ($hour) { - $date = Minz_Translate::t ('format_date_hour', $month); + $date = _t('gen.date.format_date_hour', $month); } else { - $date = Minz_Translate::t ('format_date', $month); + $date = _t('gen.date.format_date', $month); } return @date ($date, $t); @@ -106,10 +119,13 @@ function html_only_entity_decode($text) { } function customSimplePie() { + $system_conf = Minz_Configuration::get('system'); + $limits = $system_conf->limits; $simplePie = new SimplePie(); - $simplePie->set_useragent(Minz_Translate::t('freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION); + $simplePie->set_useragent(_t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION); $simplePie->set_cache_location(CACHE_PATH); - $simplePie->set_cache_duration(800); + $simplePie->set_cache_duration($limits['cache_duration']); + $simplePie->set_timeout($limits['timeout']); $simplePie->strip_htmltags(array( 'base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', @@ -164,7 +180,7 @@ function sanitizeHTML($data, $base = '') { function get_content_by_parsing ($url, $path) { require_once (LIB_PATH . '/lib_phpQuery.php'); - syslog(LOG_INFO, 'FreshRSS GET ' . $url); + Minz_Log::notice('FreshRSS GET ' . url_remove_credentials($url)); $html = file_get_contents ($url); if ($html) { @@ -201,21 +217,53 @@ function uSecString() { function invalidateHttpCache() { Minz_Session::_param('touch', uTimeString()); - return touch(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log'); -} - -function usernameFromPath($userPath) { - if (preg_match('%/([A-Za-z0-9]{1,16})_user\.php$%', $userPath, $matches)) { - return $matches[1]; - } else { - return ''; - } + return touch(join_path(DATA_PATH, 'users', Minz_Session::param('currentUser', '_'), 'log.txt')); } function listUsers() { - return array_map('usernameFromPath', glob(DATA_PATH . '/*_user.php')); + $final_list = array(); + $base_path = join_path(DATA_PATH, 'users'); + $dir_list = array_values(array_diff( + scandir($base_path), + array('..', '.', '_') + )); + + foreach ($dir_list as $file) { + if (is_dir(join_path($base_path, $file))) { + $final_list[] = $file; + } + } + + return $final_list; } + +/** + * Register and return the configuration for a given user. + * + * Note this function has been created to generate temporary configuration + * objects. If you need a long-time configuration, please don't use this function. + * + * @param $username the name of the user of which we want the configuration. + * @return a Minz_Configuration object, null if the configuration cannot be loaded. + */ +function get_user_configuration($username) { + $namespace = 'user_' . $username; + try { + Minz_Configuration::register($namespace, + join_path(USERS_PATH, $username, 'config.php'), + join_path(USERS_PATH, '_', 'config.default.php')); + } catch (Minz_ConfigurationNamespaceException $e) { + // namespace already exists, do nothing. + } catch (Minz_FileNotExistException $e) { + Minz_Log::warning($e->getMessage()); + return null; + } + + return Minz_Configuration::get($namespace); +} + + function httpAuthUser() { return isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] : ''; } @@ -238,9 +286,156 @@ function is_referer_from_same_domain() { $host = parse_url(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://') . (empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST'])); $referer = parse_url($_SERVER['HTTP_REFERER']); - if (empty($host['scheme']) || empty($referer['scheme']) || $host['scheme'] !== $referer['scheme'] || - empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) { + if (empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) { return false; } - return (isset($host['port']) ? $host['port'] : 0) === (isset($referer['port']) ? $referer['port'] : 0); + //TODO: check 'scheme', taking into account the case of a proxy + if ((isset($host['port']) ? $host['port'] : 0) !== (isset($referer['port']) ? $referer['port'] : 0)) { + return false; + } + return true; +} + + +/** + * Check PHP and its extensions are well-installed. + * + * @return array of tested values. + */ +function check_install_php() { + $pdo_mysql = extension_loaded('pdo_mysql'); + $pdo_sqlite = extension_loaded('pdo_sqlite'); + return array( + 'php' => version_compare(PHP_VERSION, '5.2.1') >= 0, + 'minz' => file_exists(LIB_PATH . '/Minz'), + 'curl' => extension_loaded('curl'), + 'pdo' => $pdo_mysql || $pdo_sqlite, + 'pcre' => extension_loaded('pcre'), + 'ctype' => extension_loaded('ctype'), + 'dom' => class_exists('DOMDocument'), + 'json' => extension_loaded('json'), + 'zip' => extension_loaded('zip'), + ); +} + + +/** + * Check different data files and directories exist. + * + * @return array of tested values. + */ +function check_install_files() { + return array( + 'data' => DATA_PATH && is_writable(DATA_PATH), + 'cache' => CACHE_PATH && is_writable(CACHE_PATH), + 'users' => USERS_PATH && is_writable(USERS_PATH), + 'favicons' => is_writable(DATA_PATH . '/favicons'), + 'persona' => is_writable(DATA_PATH . '/persona'), + 'tokens' => is_writable(DATA_PATH . '/tokens'), + ); +} + + +/** + * Check database is well-installed. + * + * @return array of tested values. + */ +function check_install_database() { + $status = array( + 'connection' => true, + 'tables' => false, + 'categories' => false, + 'feeds' => false, + 'entries' => false, + ); + + try { + $dbDAO = FreshRSS_Factory::createDatabaseDAO(); + + $status['tables'] = $dbDAO->tablesAreCorrect(); + $status['categories'] = $dbDAO->categoryIsCorrect(); + $status['feeds'] = $dbDAO->feedIsCorrect(); + $status['entries'] = $dbDAO->entryIsCorrect(); + } catch(Minz_PDOConnectionException $e) { + $status['connection'] = false; + } + + return $status; +} + +/** + * Remove a directory recursively. + * + * From http://php.net/rmdir#110489 + * + * @param $dir the directory to remove + */ +function recursive_unlink($dir) { + if (!is_dir($dir)) { + return true; + } + + $files = array_diff(scandir($dir), array('.', '..')); + foreach ($files as $filename) { + $filename = $dir . '/' . $filename; + if (is_dir($filename)) { + @chmod($filename, 0777); + recursive_unlink($filename); + } else { + unlink($filename); + } + } + + return rmdir($dir); +} + + +/** + * Remove queries where $get is appearing. + * @param $get the get attribute which should be removed. + * @param $queries an array of queries. + * @return the same array whithout those where $get is appearing. + */ +function remove_query_by_get($get, $queries) { + $final_queries = array(); + foreach ($queries as $key => $query) { + if (empty($query['get']) || $query['get'] !== $get) { + $final_queries[$key] = $query; + } + } + return $final_queries; +} + + +/** + * Add a value in an array and take care it is unique. + * @param $array the array in which we add the value. + * @param $value the value to add. + */ +function array_push_unique(&$array, $value) { + $found = array_search($value, $array) !== false; + if (!$found) { + $array[] = $value; + } +} + + +/** + * Remove a value from an array. + * @param $array the array from wich value is removed. + * @param $value the value to remove. + */ +function array_remove(&$array, $value) { + $array = array_diff($array, array($value)); +} + + +/** + * Sanitize a URL by removing HTTP credentials. + * @param $url the URL to sanitize. + * @return the same URL without HTTP credentials. + */ +function url_remove_credentials($url) { + return preg_replace('/[^\/]*:[^:]*@/', '', $url); } diff --git a/sources/p/api/greader.php b/sources/p/api/greader.php index 5a6fdad..ab1a022 100755 --- a/sources/p/api/greader.php +++ b/sources/p/api/greader.php @@ -77,7 +77,7 @@ class MyPDO extends Minz_ModelPdo { } function logMe($text) { - file_put_contents(LOG_PATH . '/api.log', $text, FILE_APPEND); + file_put_contents(join_path(USERS_PATH, '_', 'log_api.txt'), $text, FILE_APPEND); } function debugInfo() { @@ -143,24 +143,24 @@ function checkCompatibility() { exit(); } -function authorizationToUserConf() { +function authorizationToUser() { $headerAuth = headerVariable('Authorization', 'GoogleLogin_auth'); //Input is 'GoogleLogin auth', but PHP replaces spaces by '_' http://php.net/language.variables.external if ($headerAuth != '') { $headerAuthX = explode('/', $headerAuth, 2); if (count($headerAuthX) === 2) { $user = $headerAuthX[0]; if (ctype_alnum($user)) { - try { - $conf = new FreshRSS_Configuration($user); - } catch (Exception $e) { - logMe($e->getMessage() . "\n"); + $conf = get_user_configuration($user); + if (is_null($conf)) { + Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.'); unauthorized(); } - if ($headerAuthX[1] === sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash)) { - return $conf; + $system_conf = Minz_Configuration::get('system'); + if ($headerAuthX[1] === sha1($system_conf->salt . $user . $conf->apiPasswordHash)) { + return $user; } else { logMe('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1] . "\n"); - Minz_Log::record('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1], Minz_Log::WARNING); + Minz_Log::warning('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1]); unauthorized(); } } else { @@ -168,7 +168,7 @@ function authorizationToUserConf() { } } } - return null; + return ''; } function clientLogin($email, $pass) { //http://web.archive.org/web/20130604091042/http://undoc.in/clientLogin.html @@ -177,21 +177,22 @@ function clientLogin($email, $pass) { //http://web.archive.org/web/2013060409104 if (!function_exists('password_verify')) { include_once(LIB_PATH . '/password_compat.php'); } - try { - $conf = new FreshRSS_Configuration($email); - } catch (Exception $e) { - logMe($e->getMessage() . "\n"); - Minz_Log::record('Invalid API user ' . $email, Minz_Log::WARNING); + + $conf = get_user_configuration($email); + if (is_null($conf)) { + Minz_Log::warning('Invalid API user ' . $email . ': configuration cannot be found.'); unauthorized(); } + if ($conf->apiPasswordHash != '' && password_verify($pass, $conf->apiPasswordHash)) { header('Content-Type: text/plain; charset=UTF-8'); - $auth = $email . '/' . sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash); + $system_conf = Minz_Configuration::get('system'); + $auth = $email . '/' . sha1($system_conf->salt . $email . $conf->apiPasswordHash); echo 'SID=', $auth, "\n", 'Auth=', $auth, "\n"; exit(); } else { - Minz_Log::record('Password API mismatch for user ' . $email, Minz_Log::WARNING); + Minz_Log::warning('Password API mismatch for user ' . $email); unauthorized(); } } else { @@ -203,16 +204,20 @@ function clientLogin($email, $pass) { //http://web.archive.org/web/2013060409104 function token($conf) { //http://blog.martindoms.com/2009/08/15/using-the-google-reader-api-part-1/ //https://github.com/ericmann/gReader-Library/blob/master/greader.class.php - logMe('token('. $conf->user . ")\n"); //TODO: Implement real token that expires - $token = str_pad(sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash), 57, 'Z'); //Must have 57 characters + $user = Minz_Session::param('currentUser', '_'); + logMe('token('. $user . ")\n"); //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 echo $token, "\n"; exit(); } function checkToken($conf, $token) { //http://code.google.com/p/google-reader-api/wiki/ActionToken + $user = Minz_Session::param('currentUser', '_'); logMe('checkToken(' . $token . ")\n"); - if ($token === str_pad(sha1(Minz_Configuration::salt() . $conf->user . $conf->apiPasswordHash), 57, 'Z')) { + $system_conf = Minz_Configuration::get('system'); + if ($token === str_pad(sha1($system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) { return true; } unauthorized(); @@ -536,16 +541,21 @@ logMe('----------------------------------------------------------------'."\n"); $pathInfo = empty($_SERVER['PATH_INFO']) ? '/Error' : urldecode($_SERVER['PATH_INFO']); $pathInfos = explode('/', $pathInfo); -Minz_Configuration::init(); - -if (!Minz_Configuration::apiEnabled()) { +Minz_Configuration::register('system', + DATA_PATH . '/config.php', + DATA_PATH . '/config.default.php'); +$system_conf = Minz_Configuration::get('system'); +if (!$system_conf->api_enabled) { serviceUnavailable(); } Minz_Session::init('FreshRSS'); -$conf = authorizationToUserConf(); -$user = $conf == null ? '' : $conf->user; +$user = authorizationToUser(); +$conf = null; +if ($user !== '') { + $conf = get_user_configuration($user); +} logMe('User => ' . $user . "\n"); @@ -639,7 +649,7 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo markAllAsRead($streamId, $ts); break; case 'token': - Token($conf); + token($conf); break; } } elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') { diff --git a/sources/p/ext.php b/sources/p/ext.php new file mode 100755 index 0000000..5c9f912 --- /dev/null +++ b/sources/p/ext.php @@ -0,0 +1,71 @@ +setCacheDir($favicons_dir); + $favicon_url = $favicon_getter->get($website); - if (curl_getinfo ($c, CURLINFO_HTTP_CODE) == 200) { - $file = fopen ($dest, 'w'); + 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, $imgRaw); - fclose ($file); - $ok = true; + fwrite($file, $img_raw); + fclose($file); + return true; } } - curl_close ($c); - if (!$ok) { - header('Location: ' . $url); - return false; - } - return true; + + return false; } -$id = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '0'; +function show_default_favicon() { + global $default_favicon; + + header('Content-Type: image/x-icon'); + header('Content-Disposition: inline; filename="default_favicon.ico"'); + + $default_mtime = @filemtime($default_favicon); + if (!httpConditional($default_mtime, 2592000, 2)) { + readfile($default_favicon); + } +} + + +$id = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '0'; if (!ctype_xdigit($id)) { $id = '0'; } @@ -38,27 +65,28 @@ if (!ctype_xdigit($id)) { $txt = $favicons_dir . $id . '.txt'; $ico = $favicons_dir . $id . '.ico'; -$icoMTime = @filemtime($ico); -$txtMTime = @filemtime($txt); +$ico_mtime = @filemtime($ico); +$txt_mtime = @filemtime($txt); -if (($icoMTime == false) || ($txtMTime > $icoMTime)) { - if ($txtMTime == false) { - header('HTTP/1.1 404 Not Found'); - header('Content-Type: image/gif'); - readfile(PUBLIC_PATH . '/themes/icons/grey.gif'); //TODO: Better 404 favicon - die(); + +if ($ico_mtime == false || $txt_mtime > $ico_mtime) { + if ($txt_mtime == false) { + show_default_favicon(); + return; } + + // no ico file or we should download a new one. $url = file_get_contents($txt); if (!download_favicon($url, $ico)) { - die(); + // Download failed, show the default favicon + show_default_favicon(); + return; } } -require(LIB_PATH . '/http-conditional.php'); - header('Content-Type: image/x-icon'); header('Content-Disposition: inline; filename="' . $id . '.ico"'); -if (!httpConditional($icoMTime, 2592000, 2)) { +if (!httpConditional($ico_mtime, 2592000, 2)) { readfile($ico); } diff --git a/sources/p/i/index.php b/sources/p/i/index.php index 7b34eef..d3fc0b3 100755 --- a/sources/p/i/index.php +++ b/sources/p/i/index.php @@ -32,8 +32,8 @@ if (file_exists(DATA_PATH . '/do-install.txt')) { require(LIB_PATH . '/http-conditional.php'); $currentUser = Minz_Session::param('currentUser', ''); $dateLastModification = $currentUser === '' ? time() : max( - @filemtime(LOG_PATH . '/' . $currentUser . '.log'), - @filemtime(DATA_PATH . '/config.php') + @filemtime(join_path(USERS_PATH, $currentUser, 'log.txt')), + @filemtime(join_path(DATA_PATH, 'config.php')) ); if (httpConditional($dateLastModification, 0, 0, false, PHP_COMPRESSION, true)) { exit(); //No need to send anything @@ -46,7 +46,7 @@ if (file_exists(DATA_PATH . '/do-install.txt')) { $front_controller->run(); } catch (Exception $e) { echo '### Fatal error! ###
      ', "\n"; - Minz_Log::record($e->getMessage(), Minz_Log::ERROR); + Minz_Log::error($e->getMessage()); echo 'See logs files.'; } } diff --git a/sources/p/scripts/bcrypt.min.js b/sources/p/scripts/bcrypt.min.js index 6892cad..614f0c2 100755 --- a/sources/p/scripts/bcrypt.min.js +++ b/sources/p/scripts/bcrypt.min.js @@ -1,41 +1,45 @@ -/* +(function(){/* bcrypt.js (c) 2013 Daniel Wirtz Released under the Apache License, Version 2.0 see: https://github.com/dcodeIO/bcrypt.js for details */ -function p(n){throw n;}var q=null; -(function(n){function u(c,a,b,f){for(var d,e=c[a],l=c[a+1],e=e^b[0],h=0;14>=h;)d=f[e>>24&255],d+=f[256|e>>16&255],d^=f[512|e>>8&255],d+=f[768|e&255],l^=d^b[++h],d=f[l>>24&255],d+=f[256|l>>16&255],d^=f[512|l>>8&255],d+=f[768|l&255],e^=d^b[++h];c[a]=l^b[17];c[a+1]=e;return c}function s(c,a){var b,f=0;for(b=0;4>b;b++)f=f<<8|c[a]&255,a=(a+1)%c.length;return{key:f,a:a}}function y(c,a,b){for(var f=0,d=[0,0],e=a.length,l=b.length,h=0;hk;k++)for(m=0;m>1;m++)u(e,m<<1,h,g);n=[];for(k=0;k>24&255)>>>0),n.push((e[k]>>16&255)>>>0),n.push((e[k]>>8&255)>>>0),n.push((e[k]&255)>>>0);return f?(f(q,n),q):n}f&&z(d);return q}var e=B.slice(),l=e.length;(4>b||31>=8;while(a);f=f.concat(b.reverse())}return f}function w(c,a,b){function f(a){var b=[];b.push("$2");"a"<=d&&b.push(d);b.push("$");10>l&&b.push("0");b.push(l.toString());b.push("$");b.push(v.b(h,h.length));b.push(v.b(a,4*B.length-1));return b.join("")}var d,e;("$"!=a.charAt(0)||"2"!=a.charAt(1))&&p(Error("Invalid salt version: "+ -a.substring(0,2)));"$"==a.charAt(2)?(d=String.fromCharCode(0),e=3):(d=a.charAt(2),("a"!=d||"$"!=a.charAt(3))&&p(Error("Invalid salt revision: "+a.substring(2,4))),e=4);"$"=a||a>c.length)&&p(Error("Invalid 'len': "+a));b>2&63]);d=(d&3)<<4;if(b>=a){f.push(t[d&63]);break}e=c[b++]&255;d|=e>>4&15;f.push(t[d&63]);d=(e&15)<< -2;if(b>=a){f.push(t[d&63]);break}e=c[b++]&255;d|=e>>6&3;f.push(t[d&63]);f.push(t[e&63])}return f.join("")},c:function(c,a){var b=0,f=c.length,d=0,e=[],l,h,g;for(0>=a&&p(Error("Illegal 'len': "+a));b>>0;g|=(h&48)>>4;e.push(String.fromCharCode(g));if(++d>=a||b>=f)break;g=c.charCodeAt(b++);l=g>>0;g|=(l&60)>>2;e.push(String.fromCharCode(g)); -if(++d>=a||b>=f)break;g=c.charCodeAt(b++);h=g>>0;g|=h;e.push(String.fromCharCode(g));++d}f=[];for(b=0;bc||31c&&b.push("0");b.push(c.toString());b.push("$");try{b.push(v.b(G(),16)),a=b.join("")}catch(f){p(f)}return a};m.genSalt=function(c,a,b){"function"==typeof a&&(b=a,a=-1);var f;"function"==typeof c?(b=c,f=10):f=parseInt(c,10);"function"!=typeof b&&p(Error("Illegal or missing 'callback': "+b));z(function(){try{var a=m.genSaltSync(f);b(q,a)}catch(c){b(c,q)}})};m.hashSync=function(c,a){a||(a=10);"number"==typeof a&&(a=m.genSaltSync(a));return w(c,a)};m.hash=function(c,a,b){"function"!= -typeof b&&p(Error("Illegal 'callback': "+b));"number"==typeof a?m.genSalt(a,function(a,d){w(c,d,b)}):w(c,a,b)};m.compareSync=function(c,a){("string"!=typeof c||"string"!=typeof a)&&p(Error("Illegal argument types: "+typeof c+", "+typeof a));60!=a.length&&p(Error("Illegal hash length: "+a.length+" != 60"));for(var b=m.hashSync(c,a.substr(0,a.length-31)),f=b.length==a.length,d=b.length=e&&(a.length>=e&&b[e]!=a[e])&&(f=!1);return f};m.compare=function(c, -a,b){"function"!=typeof b&&p(Error("Illegal 'callback': "+b));m.hash(c,a.substr(0,29),function(c,d){b(c,a===d)})};m.getRounds=function(c){"string"!=typeof c&&p(Error("Illegal type of 'hash': "+typeof c));return parseInt(c.split("$")[2],10)};m.getSalt=function(c){"string"!=typeof c&&p(Error("Illegal type of 'hash': "+typeof c));60!=c.length&&p(Error("Illegal hash length: "+c.length+" != 60"));return c.substring(0,29)};"undefined"!=typeof module&&module.exports?module.exports=m:"undefined"!=typeof define&& -define.amd?define("bcrypt",function(){return m}):(n.dcodeIO||(n.dcodeIO={}),n.dcodeIO.bcrypt=m)})(this); +function l(t){throw t;}var p=null; +(function(t){function B(c){if("undefined"!==typeof module&&module&&module.exports)try{return require("crypto").randomBytes(c)}catch(a){}try{var b;(t.crypto||t.msCrypto).getRandomValues(b=new Uint32Array(c));return Array.prototype.slice.call(b)}catch(d){}x||l(Error("Neither WebCryptoAPI nor a crypto module is available. Use bcrypt.setRandomFallback to set an alternative"));return x(c)}function F(c){var a=[],b=0;G.f(function(){return b>=c.length?p:c.charCodeAt(b++)},function(b){a.push(b)});return a} +function y(c,a){var b=0,d=[],f,e;for((0>=a||a>c.length)&&l(Error("Illegal len: "+a));b>2&63]);f=(f&3)<<4;if(b>=a){d.push(u[f&63]);break}e=c[b++]&255;f|=e>>4&15;d.push(u[f&63]);f=(e&15)<<2;if(b>=a){d.push(u[f&63]);break}e=c[b++]&255;f|=e>>6&3;d.push(u[f&63]);d.push(u[e&63])}return d.join("")}function H(c){for(var a=0,b=c.length,d=0,f=[],e,k,h;ad;){h=c.charCodeAt(a++);e=h>>0; +h|=(k&48)>>4;f.push(z(h));if(16<=++d||a>=b)break;h=c.charCodeAt(a++);e=h>>0;h|=(e&60)>>2;f.push(z(h));if(16<=++d||a>=b)break;h=c.charCodeAt(a++);k=h>>0;h|=k;f.push(z(h));++d}c=[];for(a=0;a=h;)f=d[e>>24&255],f+=d[256|e>>16&255],f^=d[512|e>>8&255],f+=d[768|e&255],k^=f^b[++h],f=d[k>>24&255],f+=d[256|k>>16&255],f^=d[512|k>>8& +255],f+=d[768|k&255],e^=f^b[++h];c[a]=k^b[17];c[a+1]=e;return c}function v(c,a){for(var b=0,d=0;4>b;++b)d=d<<8|c[a]&255,a=(a+1)%c.length;return{key:d,a:a}}function C(c,a,b){for(var d=0,f=[0,0],e=a.length,k=b.length,h,g=0;gq;q++)for(r=0;r>1;r++)w(k,r<<1,m,n);g=[];for(q=0;q>24&255)>>>0),g.push((k[q]>>16&255)>>>0),g.push((k[q]>>8&255)>>>0),g.push((k[q]&255)>>>0);if(d){d(p,g);return}return g}d&& +s(e)}var k=E.slice(),h=k.length,g;if(4>b||31g&&b.push("0");b.push(g.toString());b.push("$"); +b.push(y(m,m.length));b.push(y(a,4*E.length-1));return b.join("")}var e;if("string"!==typeof c||"string"!==typeof a){e=Error("Invalid string / salt: Not a string");if(b){s(b.bind(this,e));return}l(e)}var k,h;if("$"!==a.charAt(0)||"2"!==a.charAt(1)){e=Error("Invalid salt version: "+a.substring(0,2));if(b){s(b.bind(this,e));return}l(e)}if("$"===a.charAt(2))k=String.fromCharCode(0),h=3;else{k=a.charAt(2);if("a"!==k&&"y"!==k||"$"!==a.charAt(3)){e=Error("Invalid salt revision: "+a.substring(2,4));if(b){s(b.bind(this, +e));return}l(e)}h=4}if("$"c||31c&&b.push("0");b.push(c.toString());b.push("$");b.push(y(B(16),16));return b.join("")};n.genSalt=function(c,a,b){"function"===typeof a&&(b=a,a=void 0);"function"===typeof c&&(b=c,c=10);"function"!==typeof b&&l(Error("Illegal callback: "+typeof b));"number"!==typeof c?s(b.bind(this,Error("Illegal arguments: "+typeof c))):s(function(){try{b(p,n.genSaltSync(c))}catch(a){b(a)}})};n.hashSync=function(c, +a){"undefined"===typeof a&&(a=10);"number"===typeof a&&(a=n.genSaltSync(a));("string"!==typeof c||"string"!==typeof a)&&l(Error("Illegal arguments: "+typeof c+", "+typeof a));return A(c,a)};n.hash=function(c,a,b,d){"function"!==typeof b&&l(Error("Illegal callback: "+typeof b));"string"===typeof c&&"number"===typeof a?n.genSalt(a,function(a,e){A(c,e,b,d)}):"string"===typeof c&&"string"===typeof a?A(c,a,b,d):s(b.bind(this,Error("Illegal arguments: "+typeof c+", "+typeof a)))};n.compareSync=function(c, +a){("string"!==typeof c||"string"!==typeof a)&&l(Error("Illegal arguments: "+typeof c+", "+typeof a));if(60!==a.length)return!1;for(var b=n.hashSync(c,a.substr(0,a.length-31)),d=b.length===a.length,f=b.length=e&&(a.length>=e&&b[e]!=a[e])&&(d=!1);return d};n.compare=function(c,a,b,d){"function"!==typeof b&&l(Error("Illegal callback: "+typeof b));"string"!==typeof c||"string"!==typeof a?s(b.bind(this,Error("Illegal arguments: "+typeof c+", "+typeof a))): +n.hash(c,a.substr(0,29),function(d,c){b(d,a===c)},d)};n.getRounds=function(c){"string"!==typeof c&&l(Error("Illegal arguments: "+typeof c));return parseInt(c.split("$")[2],10)};n.getSalt=function(c){"string"!==typeof c&&l(Error("Illegal arguments: "+typeof c));60!==c.length&&l(Error("Illegal hash length: "+c.length+" != 60"));return c.substring(0,29)};var s="undefined"!==typeof process&&process&&"function"===typeof process.nextTick?"function"===typeof setImmediate?setImmediate:process.nextTick:setTimeout, +u="./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split(""),r=[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,54,55,56,57,58,59,60,61,62,63,-1,-1,-1,-1,-1,-1,-1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,-1,-1,-1,-1,-1,-1,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,-1,-1,-1,-1,-1],z=String.fromCharCode,G=function(){var c={h:1114111, +g:function(a,b){var d=p;"number"===typeof a&&(d=a,a=function(){return p});for(;d!==p||(d=a())!==p;)128>d?b(d&127):(2048>d?b(d>>6&31|192):(65536>d?b(d>>12&15|224):(b(d>>18&7|240),b(d>>12&63|128)),b(d>>6&63|128)),b(d&63|128)),d=p},e:function(a,b){function d(a){a=a.slice(0,a.indexOf(p));var b=Error(a.toString());b.name="TruncatedError";b.bytes=a;l(b)}for(var c,e,k,h;(c=a())!==p;)0===(c&128)?b(c):192===(c&224)?((e=a())===p&&d([c,e]),b((c&31)<<6|e&63)):224===(c&240)?(((e=a())===p||(k=a())===p)&&d([c,e, +k]),b((c&15)<<12|(e&63)<<6|k&63)):240===(c&248)?(((e=a())===p||(k=a())===p||(h=a())===p)&&d([c,e,k,h]),b((c&7)<<18|(e&63)<<12|(k&63)<<6|h&63)):l(RangeError("Illegal starting byte: "+c))},b:function(a,b){for(var c,f=p;(c=f!==p?f:a())!==p;)55296<=c&&57343>=c&&(f=a())!==p&&56320<=f&&57343>=f?(b(1024*(c-55296)+f-56320+65536),f=p):b(c);f!==p&&b(f)},d:function(a,b){var c=p;"number"===typeof a&&(c=a,a=function(){return p});for(;c!==p||(c=a())!==p;)65535>=c?b(c):(c-=65536,b((c>>10)+55296),b(c%1024+56320)), +c=p},f:function(a,b){c.b(a,function(a){c.g(a,b)})},k:function(a,b){c.e(a,function(a){c.d(a,b)})},c:function(a){return 128>a?1:2048>a?2:65536>a?3:4},j:function(a){for(var b,d=0;(b=a())!==p;)d+=c.c(b);return d},i:function(a){var b=0,d=0;c.b(a,function(a){++b;d+=c.c(a)});return[b,d]}};return c}();Date.now=Date.now||function(){return+new Date};var J=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069, +3041331479,2450970073,2306472731],K=[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828, +289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486, +1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557, +442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005,3532540348,1274779536,3789419226,2764799539,1660621633,3471099624,4011903706,913787905,3497959166,737222580,2514213453,2928710040,3937242737,1804850592, +3499020752,2949064160,2386320175,2390070455,2415321851,4061277028,2290661394,2416832540,1336762016,1754252060,3520065937,3014181293,791618072,3188594551,3933548030,2332172193,3852520463,3043980520,413987798,3465142937,3030929376,4245938359,2093235073,3534596313,375366246,2157278981,2479649556,555357303,3870105701,2008414854,3344188149,4221384143,3956125452,2067696032,3594591187,2921233993,2428461,544322398,577241275,1471733935,610547355,4027169054,1432588573,1507829418,2025931657,3646575487,545086370, +48609733,2200306550,1653985193,298326376,1316178497,3007786442,2064951626,458293330,2589141269,3591329599,3164325604,727753846,2179363840,146436021,1461446943,4069977195,705550613,3059967265,3887724982,4281599278,3313849956,1404054877,2845806497,146425753,1854211946,1266315497,3048417604,3681880366,3289982499,290971E4,1235738493,2632868024,2414719590,3970600049,1771706367,1449415276,3266420449,422970021,1963543593,2690192192,3826793022,1062508698,1531092325,1804592342,2583117782,2714934279,4024971509, +1294809318,4028980673,1289560198,2221992742,1669523910,35572830,157838143,1052438473,1016535060,1802137761,1753167236,1386275462,3080475397,2857371447,1040679964,2145300060,2390574316,1461121720,2956646967,4031777805,4028374788,33600511,2920084762,1018524850,629373528,3691585981,3515945977,2091462646,2486323059,586499841,988145025,935516892,3367335476,2599673255,2839830854,265290510,3972581182,2759138881,3795373465,1005194799,847297441,406762289,1314163512,1332590856,1866599683,4127851711,750260880, +613907577,1450815602,3165620655,3734664991,3650291728,3012275730,3704569646,1427272223,778793252,1343938022,2676280711,2052605720,1946737175,3164576444,3914038668,3967478842,3682934266,1661551462,3294938066,4011595847,840292616,3712170807,616741398,312560963,711312465,1351876610,322626781,1910503582,271666773,2175563734,1594956187,70604529,3617834859,1007753275,1495573769,4069517037,2549218298,2663038764,504708206,2263041392,3941167025,2249088522,1514023603,1998579484,1312622330,694541497,2582060303, +2151582166,1382467621,776784248,2618340202,3323268794,2497899128,2784771155,503983604,4076293799,907881277,423175695,432175456,1378068232,4145222326,3954048622,3938656102,3820766613,2793130115,2977904593,26017576,3274890735,3194772133,1700274565,1756076034,4006520079,3677328699,720338349,1533947780,354530856,688349552,3973924725,1637815568,332179504,3949051286,53804574,2852348879,3044236432,1282449977,3583942155,3416972820,4006381244,1617046695,2628476075,3002303598,1686838959,431878346,2686675385, +1700445008,1080580658,1009431731,832498133,3223435511,2605976345,2271191193,2516031870,1648197032,4164389018,2548247927,300782431,375919233,238389289,3353747414,2531188641,2019080857,1475708069,455242339,2609103871,448939670,3451063019,1395535956,2413381860,1841049896,1491858159,885456874,4264095073,4001119347,1565136089,3898914787,1108368660,540939232,1173283510,2745871338,3681308437,4207628240,3343053890,4016749493,1699691293,1103962373,3625875870,2256883143,3830138730,1031889488,3479347698,1535977030, +4236805024,3251091107,2132092099,1774941330,1199868427,1452454533,157007616,2904115357,342012276,595725824,1480756522,206960106,497939518,591360097,863170706,2375253569,3596610801,1814182875,2094937945,3421402208,1082520231,3463918190,2785509508,435703966,3908032597,1641649973,2842273706,3305899714,1510255612,2148256476,2655287854,3276092548,4258621189,236887753,3681803219,274041037,1734335097,3815195456,3317970021,1899903192,1026095262,4050517792,356393447,2410691914,3873677099,3682840055,3913112168, +2491498743,4132185628,2489919796,1091903735,1979897079,3170134830,3567386728,3557303409,857797738,1136121015,1342202287,507115054,2535736646,337727348,3213592640,1301675037,2528481711,1895095763,1721773893,3216771564,62756741,2142006736,835421444,2531993523,1442658625,3659876326,2882144922,676362277,1392781812,170690266,3921047035,1759253602,3611846912,1745797284,664899054,1329594018,3901205900,3045908486,2062866102,2865634940,3543621612,3464012697,1080764994,553557557,3656615353,3996768171,991055499, +499776247,1265440854,648242737,3940784050,980351604,3713745714,1749149687,3396870395,4211799374,3640570775,1161844396,3125318951,1431517754,545492359,4268468663,3499529547,1437099964,2702547544,3433638243,2581715763,2787789398,1060185593,1593081372,2418618748,4260947970,69676912,2159744348,86519011,2512459080,3838209314,1220612927,3339683548,133810670,1090789135,1078426020,1569222167,845107691,3583754449,4072456591,1091646820,628848692,1613405280,3757631651,526609435,236106946,48312990,2942717905, +3402727701,1797494240,859738849,992217954,4005476642,2243076622,3870952857,3732016268,765654824,3490871365,2511836413,1685915746,3888969200,1414112111,2273134842,3281911079,4080962846,172450625,2569994100,980381355,4109958455,2819808352,2716589560,2568741196,3681446669,3329971472,1835478071,660984891,3704678404,4045999559,3422617507,3040415634,1762651403,1719377915,3470491036,2693910283,3642056355,3138596744,1364962596,2073328063,1983633131,926494387,3423689081,2150032023,4096667949,1749200295,3328846651, +309677260,2016342300,1779581495,3079819751,111262694,1274766160,443224088,298511866,1025883608,3806446537,1145181785,168956806,3641502830,3584813610,1689216846,3666258015,3200248200,1692713982,2646376535,4042768518,1618508792,1610833997,3523052358,4130873264,2001055236,3610705100,2202168115,4028541809,2961195399,1006657119,2006996926,3186142756,1430667929,3210227297,1314452623,4074634658,4101304120,2273951170,1399257539,3367210612,3027628629,1190975929,2062231137,2333990788,2221543033,2438960610, +1181637006,548689776,2362791313,3372408396,3104550113,3145860560,296247880,1970579870,3078560182,3769228297,1714227617,3291629107,3898220290,166772364,1251581989,493813264,448347421,195405023,2709975567,677966185,3703036547,1463355134,2715995803,1338867538,1343315457,2802222074,2684532164,233230375,2599980071,2000651841,3277868038,1638401717,4028070440,3237316320,6314154,819756386,300326615,590932579,1405279636,3267499572,3150704214,2428286686,3959192993,3461946742,1862657033,1266418056,963775037, +2089974820,2263052895,1917689273,448879540,3550394620,3981727096,150775221,3627908307,1303187396,508620638,2975983352,2726630617,1817252668,1876281319,1457606340,908771278,3720792119,3617206836,2455994898,1729034894,1080033504,976866871,3556439503,2881648439,1522871579,1555064734,1336096578,3548522304,2579274686,3574697629,3205460757,3593280638,3338716283,3079412587,564236357,2993598910,1781952180,1464380207,3163844217,3332601554,1699332808,1393555694,1183702653,3581086237,1288719814,691649499,2847557200, +2895455976,3193889540,2717570544,1781354906,1676643554,2592534050,3230253752,1126444790,2770207658,2633158820,2210423226,2615765581,2414155088,3127139286,673620729,2805611233,1269405062,4015350505,3341807571,4149409754,1057255273,2012875353,2162469141,2276492801,2601117357,993977747,3918593370,2654263191,753973209,36408145,2530585658,25011837,3520020182,2088578344,530523599,2918365339,1524020338,1518925132,3760827505,3759777254,1202760957,3985898139,3906192525,674977740,4174734889,2031300136,2019492241, +3983892565,4153806404,3822280332,352677332,2297720250,60907813,90501309,3286998549,1016092578,2535922412,2839152426,457141659,509813237,4120667899,652014361,1966332200,2975202805,55981186,2327461051,676427537,3255491064,2882294119,3433927263,1307055953,942726286,933058658,2468411793,3933900994,4215176142,1361170020,2001714738,2830558078,3274259782,1222529897,1679025792,2729314320,3714953764,1770335741,151462246,3013232138,1682292957,1483529935,471910574,1539241949,458788160,3436315007,1807016891, +3718408830,978976581,1043663428,3165965781,1927990952,4200891579,2372276910,3208408903,3533431907,1412390302,2931980059,4132332400,1947078029,3881505623,4168226417,2941484381,1077988104,1320477388,886195818,18198404,3786409E3,2509781533,112762804,3463356488,1866414978,891333506,18488651,661792760,1628790961,3885187036,3141171499,876946877,2693282273,1372485963,791857591,2686433993,3759982718,3167212022,3472953795,2716379847,445679433,3561995674,3504004811,3574258232,54117162,3331405415,2381918588, +3769707343,4154350007,1140177722,4074052095,668550556,3214352940,367459370,261225585,2610173221,4209349473,3468074219,3265815641,314222801,3066103646,3808782860,282218597,3406013506,3773591054,379116347,1285071038,846784868,2669647154,3771962079,3550491691,2305946142,453669953,1268987020,3317592352,3279303384,3744833421,2610507566,3859509063,266596637,3847019092,517658769,3462560207,3443424879,370717030,4247526661,2224018117,4143653529,4112773975,2788324899,2477274417,1456262402,2901442914,1517677493, +1846949527,2295493580,3734397586,2176403920,1280348187,1908823572,3871786941,846861322,1172426758,3287448474,3383383037,1655181056,3139813346,901632758,1897031941,2986607138,3066810236,3447102507,1393639104,373351379,950779232,625454576,3124240540,4148612726,2007998917,544563296,2244738638,2330496472,2058025392,1291430526,424198748,50039436,29584100,3605783033,2429876329,2791104160,1057563949,3255363231,3075367218,3463963227,1469046755,985887462],E=[1332899944,1700884034,1701343084,1684370003,1668446532, +1869963892];"undefined"!==typeof module&&module.exports?module.exports=n:"undefined"!==typeof define&&define.amd?define(function(){return n}):(t.dcodeIO=t.dcodeIO||{}).bcrypt=n})(this);})(); diff --git a/sources/p/scripts/category.js b/sources/p/scripts/category.js new file mode 100755 index 0000000..c33e685 --- /dev/null +++ b/sources/p/scripts/category.js @@ -0,0 +1,118 @@ +"use strict"; + +var loading = false, + dnd_successful = false; + +function dragend_process(t) { + t.style.display = 'none'; + + if (loading) { + window.setTimeout(function() { + dragend_process(t); + }, 50); + } + + if (!dnd_successful) { + t.style.display = 'block'; + t.style.opacity = 1.0; + } else { + var parent = $(t.parentNode); + $(t).remove(); + + if (parent.children().length <= 0) { + parent.append('
    • ' + i18n['category_empty'] + '
    • '); + } + } +} + +function init_draggable() { + if (!(window.$ && window.i18n)) { + if (window.console) { + console.log('FreshRSS waiting for JS…'); + } + window.setTimeout(init_draggable, 50); + return; + } + + $.event.props.push('dataTransfer'); + + var draggable = '[draggable="true"]', + dropzone = '[dropzone="move"]'; + + $('.drop-section').on('dragstart', draggable, function(e) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/html', e.target.outerHTML); + e.dataTransfer.setData('text', e.target.getAttribute('data-feed-id')); + e.target.style.opacity = 0.3; + + dnd_successful = false; + }); + $('.drop-section').on('dragend', draggable, function(e) { + dragend_process(e.target); + }); + + $('.drop-section').on('dragenter', dropzone, function(e) { + $(this).addClass('drag-hover'); + + e.preventDefault(); + }); + $('.drop-section').on('dragleave', dropzone, function(e) { + var pos_this = $(this).position(), + scroll_top = $(document).scrollTop(), + top = pos_this.top, + left = pos_this.left, + right = left + $(this).width(), + bottom = top + $(this).height(), + mouse_x = e.originalEvent.screenX, + mouse_y = e.originalEvent.clientY + scroll_top; + + if (left <= mouse_x && mouse_x <= right && + top <= mouse_y && mouse_y <= bottom) { + // HACK because dragleave is triggered when hovering children! + return; + } + $(this).removeClass('drag-hover'); + }); + $('.drop-section').on('dragover', dropzone, function(e) { + e.dataTransfer.dropEffect = "move"; + + e.preventDefault(); + return false; + }); + $('.drop-section').on('drop', dropzone, function(e) { + var feed_id = e.dataTransfer.getData('text'), + cat_id = e.target.parentNode.getAttribute('data-cat-id'); + + loading = true; + + $.ajax({ + type: 'POST', + url: './?c=feed&a=move', + data : { + f_id: feed_id, + c_id: cat_id + } + }).success(function() { + $(e.target).after(e.dataTransfer.getData('text/html')); + if ($(e.target).hasClass('disabled')) { + $(e.target).remove(); + } + dnd_successful = true; + }).complete(function() { + loading = false; + }); + + $(this).removeClass('drag-hover'); + + e.preventDefault(); + }); +} + + +if (document.readyState && document.readyState !== 'loading') { + init_draggable(); +} else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', function () { + init_draggable(); + }, false); +} diff --git a/sources/p/scripts/global_view.js b/sources/p/scripts/global_view.js index 7105520..7d7ba22 100755 --- a/sources/p/scripts/global_view.js +++ b/sources/p/scripts/global_view.js @@ -9,7 +9,7 @@ function load_panel(link) { panel_loading = true; $.get(link, function (data) { - $("#panel").append($(".nav_menu, #stream .day, #stream .flux, #stream .pagination", data)); + $("#panel").append($(".nav_menu, #stream .day, #stream .flux, #stream .pagination, #stream.prompt", data)); $("#panel .nav_menu").children().not("#nav_menu_read_all").remove(); @@ -24,12 +24,13 @@ function load_panel(link) { // en en ouvrant une autre ensuite, on se retrouve au même point de scroll $("#panel").scrollTop(0); - $('#panel').on('click', '#nav_menu_read_all > a, #nav_menu_read_all .item > a, #bigMarkAsRead', function () { + $('#panel').on('click', '#nav_menu_read_all button, #bigMarkAsRead', function () { + console.log($(this).attr("formaction")); $.ajax({ - url: $(this).attr("href"), + type: "POST", + url: $(this).attr("formaction"), async: false }); - //$("#panel .close").first().click(); window.location.reload(false); return false; }); @@ -39,9 +40,8 @@ function load_panel(link) { } function init_close_panel() { - $("#panel .close").click(function () { - $("#panel").html('' + window.iconClose + ''); - init_close_panel(); + $("#overlay .close").click(function () { + $("#panel").html(''); $("#panel").slideToggle(); $("#overlay").fadeOut(); @@ -50,7 +50,8 @@ function init_close_panel() { } function init_global_view() { - $("#stream .box-category a").click(function () { + // TODO: should be based on generic classes. + $(".box a").click(function () { var link = $(this).attr("href"); load_panel(link); diff --git a/sources/p/scripts/jquery.min.js b/sources/p/scripts/jquery.min.js index e5ace11..25714ed 100755 --- a/sources/p/scripts/jquery.min.js +++ b/sources/p/scripts/jquery.min.js @@ -1,4 +1,4 @@ -/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(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 ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
      ",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.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},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.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=fb.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=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.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]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.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+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(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),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(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 tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(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?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) -},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
      "],col:[2,"","
      "],tr:[2,"","
      "],td:[3,"","
      "],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("