diff options
| author | 2014-09-08 19:26:35 +0200 | |
|---|---|---|
| committer | 2014-09-08 19:26:35 +0200 | |
| commit | ef1b35fc4385c99c4d38e3f87e8126d0dbe21519 (patch) | |
| tree | c2127f92281084c3cb28f635dea63a9a179eabbb | |
| parent | 909d8747ba09f9c9a6ac895f1f4f0763bdb27a55 (diff) | |
| parent | c3fd8877c021b86180b3bea4d4260e6478f0558e (diff) | |
Merge branch 'dev' into 411-update-system
Conflicts:
constants.php
44 files changed, 1221 insertions, 377 deletions
@@ -11,9 +11,12 @@ * Improvements * Security * Basic protection against XSRF (Cross-Site Request Forgery) based on HTTP Referer (POST requests only) +* API + * Compatible with lighttpd * Misc. * Changed lazyload implementation * Support of HTML5 notifications for new upcoming articles + * Add option to stay logged in * Bux fixes in export function, add/remove users, keyboard shortcuts, etc. diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 000000000..df43ef7e8 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,101 @@ +* [English version](README.md) + +# FreshRSS +FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/). + +Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. + +Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme. + +* Site officiel : http://freshrss.org +* Démo : http://demo.freshrss.org/ +* Développeur : Marien Fressinaud <dev@marienfressinaud.fr> +* Version actuelle : 0.8-dev +* Date de publication 2014-0x-xx +* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) + + + +# 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 ! + +# 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) + +# Pré-requis +* Serveur modeste, par exemple sous Linux ou Windows + * Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées) +* Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres) +* PHP 5.2.1+ (PHP 5.3.7+ recommandé) + * Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (seulement pour accès API sur platformes < 64 bits) + * Recommandés : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip) +* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ +* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ + * Fonctionne aussi sur mobile + + + +# 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) +2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`) +3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/` +4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation +5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à me contacter. + +# Contrôle d’accès +Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix : +* En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+) +* En utilisant l’identification par [Mozilla Persona](https://login.persona.org/about) incluse dans FreshRSS +* En utilisant un contrôle d’accès HTTP défini par votre serveur Web + * Voir par exemple la [documentation d’Apache sur l’authentification](http://httpd.apache.org/docs/trunk/howto/auth.html) + * Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant. + +# Rafraîchissement automatique des flux +* Vous pouvez ajouter une tâche Cron lançant régulièrement le script d’actualisation automatique des flux. +Consultez la documentation de Cron de votre système d’exploitation ([Debian/Ubuntu](http://doc.ubuntu-fr.org/cron), [Red Hat/Fedora](http://doc.fedora-fr.org/wiki/CRON_:_Configuration_de_t%C3%A2ches_automatis%C3%A9es), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](http://wiki.gentoo.org/wiki/Cron/fr), [Arch Linux](http://wiki.archlinux.fr/Cron)…). +C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web (souvent “www-data”). +Par exemple, pour exécuter le script toutes les heures : + +``` +7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 +``` + +# Conseils +* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`. + * En particulier, les données personnelles se trouvent dans le répertoire `./data/`. +* Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici. +* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`. + +# Sauvegarde +* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php` et éventuellement `./data/persona/` +* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML +* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL : + +```bash +mysqldump -u utilisateur -p --databases freshrss > freshrss.sql +``` + + +# Bibliothèques incluses +* [SimplePie](http://simplepie.org/) +* [MINZ](https://github.com/marienfressinaud/MINZ) +* [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/) +* [jQuery](http://jquery.com/) +* [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/) +* [flotr2](http://www.humblesoftware.com/flotr2) + +## Uniquement pour certaines options +* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js) +* [phpQuery](http://code.google.com/p/phpquery/) + +## Si les fonctions natives ne sont pas disponibles +* [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198) +* [password_compat](https://github.com/ircmaxell/password_compat) @@ -1,88 +1,90 @@ +* [Version française](README.fr.md) + # FreshRSS -FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/). +FreshRSS is a self-hosted RSS feed agregator like [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](http://tontof.net/kriss/feed/). -Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. +It is at the same time light-weight, easy to work with, powerful and customizable. -Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme. +It is a multi-user application with an anonymous reading mode. -* Site officiel : http://freshrss.org -* Démo : http://demo.freshrss.org/ -* Développeur : Marien Fressinaud <dev@marienfressinaud.fr> -* Version actuelle : 0.8-dev -* Date de publication 2014-0x-xx +* Official website: http://freshrss.org +* Demo: http://demo.freshrss.org/ +* Developer: Marien Fressinaud <dev@marienfressinaud.fr> +* Current version: 0.8-dev +* Publication date: 2014-0x-xx * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) - + -# Note sur les branches -**Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond : +# Note on branches +**This application is still in development!** Please use the branch that suits your needs: -* 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 ! +* 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! # 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) - -# Pré-requis -* Serveur modeste, par exemple sous Linux ou Windows - * Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées) -* Serveur Web Apache2 ou Nginx (non testé sur les autres) -* PHP 5.2.1+ (PHP 5.3.7+ recommandé) - * Requis : [PDO_MySQL](http://php.net/pdo-mysql), [cURL](http://php.net/curl), [LibXML](http://php.net/xml), [PCRE](http://php.net/pcre), [ctype](http://php.net/ctype) - * Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv), [Zip](http://php.net/zip) -* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ (en bêta) -* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ - * Fonctionne aussi sur mobile - - +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) + +# Requirements +* Light server running Linux or Windows + * It even works on Raspberry Pi with response time under a second (tested with 150 feeds, 22k articles, or 32Mo of compressed data) +* A web server: Apache2 (recommanded), nginx, lighttpd (not tested on others) +* PHP 5.2.1+ (PHP 5.3.7+ recommanded) + * 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+ +* A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+ + * Works on mobile + + # 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) -2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`) -3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/` -4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation -5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à me contacter. - -# Contrôle d’accès -Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix : -* En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP 5.3.3+) -* En utilisant l’identification par [Mozilla Persona](https://login.persona.org/about) incluse dans FreshRSS -* En utilisant un contrôle d’accès HTTP défini par votre serveur Web - * Voir par exemple la [documentation d’Apache sur l’authentification](http://httpd.apache.org/docs/trunk/howto/auth.html) - * Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant. - -# Rafraîchissement automatique des flux -* Vous pouvez ajouter une tâche Cron lançant régulièrement le script d’actualisation automatique des flux. -Consultez la documentation de Cron de votre système d’exploitation ([Debian/Ubuntu](http://doc.ubuntu-fr.org/cron), [Red Hat/Fedora](http://doc.fedora-fr.org/wiki/CRON_:_Configuration_de_t%C3%A2ches_automatis%C3%A9es), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](http://wiki.gentoo.org/wiki/Cron/fr), [Arch Linux](http://wiki.archlinux.fr/Cron)…). -C’est une bonne idée d’utiliser le même utilisateur que votre serveur Web (souvent “www-data”). -Par exemple, pour exécuter le script toutes les heures : +1. Get FreshRSS with git or [by downloading the archive](https://github.com/marienfressinaud/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 +5. Every thing should be working :) If you encounter any problem, feel free to contact me. + +# Access control +It is needed for the multi-user mode to limit access to FreshRSS. You can: +* use form authentication (need JavaScript and PHP 5.3.7+, works with some PHP 5.3.3+) +* use [Mozilla Persona](https://login.persona.org/about) authentication included in FreshRSS +* use HTTP authentication supported by your web server + * See [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html) + * In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file. + +# Automatic feed update +* You can add a Cron job to launch the update script. +Check the Cron documentation related to your distribution ([Debian/Ubuntu](https://help.ubuntu.com/community/CronHowto), [Red Hat/Fedora](https://fedoraproject.org/wiki/Administration_Guide_Draft/Cron), [Slackware](http://docs.slackware.com/fr:slackbook:process_control?#cron), [Gentoo](https://wiki.gentoo.org/wiki/Cron), [Arch Linux](https://wiki.archlinux.org/index.php/Cron)…). +It’s a good idea to use the web server user . +For example, if you want to run the script every hour: ``` 7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 ``` -# Conseils -* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`. - * En particulier, les données personnelles se trouvent dans le répertoire `./data/`. -* Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici. -* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`. +# 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. -# Sauvegarde -* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php` et éventuellement `./data/persona/` -* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML -* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL : +# Backup +* You need to keep `./data/config.php`, `./data/*_user.php` and `./data/persona/` files +* You can export your feed list in OPML format from FreshRSS +* To save articles, you can use [phpMyAdmin](http://www.phpmyadmin.net) or MySQL tools: ```bash -mysqldump -u utilisateur -p --databases freshrss > freshrss.sql +mysqldump -u user -p --databases freshrss > freshrss.sql ``` -# Bibliothèques incluses +# Included libraries * [SimplePie](http://simplepie.org/) * [MINZ](https://github.com/marienfressinaud/MINZ) * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/) @@ -90,10 +92,10 @@ mysqldump -u utilisateur -p --databases freshrss > freshrss.sql * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/) * [flotr2](http://www.humblesoftware.com/flotr2) -## Uniquement pour certaines options +## Only for some options * [bcrypt.js](https://github.com/dcodeIO/bcrypt.js) * [phpQuery](http://code.google.com/p/phpquery/) -## Si les fonctions natives ne sont pas disponibles +## If native functions are not available * [Services_JSON](http://pear.php.net/pepr/pepr-proposal-show.php?id=198) * [password_compat](https://github.com/ircmaxell/password_compat) diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 0bf58880f..bb96bfae3 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -184,6 +184,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $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)); diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 2b3353d93..5adf3878a 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -5,7 +5,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if (!$this->view->loginOk) { Minz_Error::error( 403, - array('error' => array(Minz_Translate::t('access_denied'))) + array('error' => array(_t('access_denied'))) ); } @@ -20,33 +20,51 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->view->categories = $this->catDAO->listCategories(); $this->view->feeds = $this->feedDAO->listFeeds(); - Minz_View::prependTitle(Minz_Translate::t('import_export') . ' · '); + Minz_View::prependTitle(_t('import_export') . ' · '); } public function importAction() { - if (Minz_Request::isPost() && $_FILES['file']['error'] == 0) { - @set_time_limit(300); + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); + } - $file = $_FILES['file']; - $type_file = $this->guessFileType($file['name']); + $file = $_FILES['file']; + $status_file = $file['error']; - $list_files = array( - 'opml' => array(), - 'json_starred' => array(), - 'json_feed' => array() - ); + if ($status_file !== 0) { + Minz_Log::error('File cannot be uploaded. Error code: ' . $status_file); + Minz_Request::bad(_t('file_cannot_be_uploaded'), + array('c' => 'importExport', 'a' => 'index')); + } - // We try to list all files according to their type - // A zip file is first opened and then its files are listed - $list = array(); - if ($type_file === 'zip') { - $zip = zip_open($file['tmp_name']); + @set_time_limit(300); - while (($zipfile = zip_read($zip)) !== false) { - $type_zipfile = $this->guessFileType( - zip_entry_name($zipfile) - ); + $type_file = $this->guessFileType($file['name']); + $list_files = array( + 'opml' => array(), + 'json_starred' => array(), + 'json_feed' => array() + ); + + // We try to list all files according to their type + $list = array(); + if ($type_file === 'zip' && extension_loaded('zip')) { + $zip = zip_open($file['tmp_name']); + + 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'), + array('c' => 'importExport', 'a' => 'index')); + } + + while (($zipfile = zip_read($zip)) !== false) { + if (!is_resource($zipfile)) { + // zip_entry() can also return an error code! + Minz_Log::error('Zip file cannot be imported. Error code: ' . $zipfile); + } else { + $type_zipfile = $this->guessFileType(zip_entry_name($zipfile)); if ($type_file !== 'unknown') { $list_files[$type_zipfile][] = zip_entry_read( $zipfile, @@ -54,59 +72,37 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { ); } } - - zip_close($zip); - } elseif ($type_file !== 'unknown') { - $list_files[$type_file][] = file_get_contents( - $file['tmp_name'] - ); - } - - // Import different files. - // OPML first(so categories and feeds are imported) - // Starred articles then so the "favourite" status is already set - // And finally all other files. - $error = false; - foreach ($list_files['opml'] as $opml_file) { - $error = $this->importOpml($opml_file); - } - foreach ($list_files['json_starred'] as $article_file) { - $error = $this->importArticles($article_file, true); - } - foreach ($list_files['json_feed'] as $article_file) { - $error = $this->importArticles($article_file); } - // And finally, we get import status and redirect to the home page - $notif = null; - if ($error === true) { - $content_notif = Minz_Translate::t( - 'feeds_imported_with_errors' - ); - } else { - $content_notif = Minz_Translate::t( - 'feeds_imported' - ); - } - - Minz_Session::_param('notification', array( - 'type' => 'good', - 'content' => $content_notif - )); - Minz_Session::_param('actualize_feeds', true); + zip_close($zip); + } elseif ($type_file === 'zip') { + // Zip extension is not loaded + Minz_Request::bad(_t('no_zip_extension'), + array('c' => 'importExport', 'a' => 'index')); + } elseif ($type_file !== 'unknown') { + $list_files[$type_file][] = file_get_contents($file['tmp_name']); + } - Minz_Request::forward(array( - 'c' => 'index', - 'a' => 'index' - ), true); + // Import file contents. + // OPML first(so categories and feeds are imported) + // Starred articles then so the "favourite" status is already set + // And finally all other files. + $error = false; + foreach ($list_files['opml'] as $opml_file) { + $error = $this->importOpml($opml_file); + } + foreach ($list_files['json_starred'] as $article_file) { + $error = $this->importArticles($article_file, true); + } + foreach ($list_files['json_feed'] as $article_file) { + $error = $this->importArticles($article_file); } - // What are you doing? you have to call this controller - // with a POST request! - Minz_Request::forward(array( - 'c' => 'importExport', - 'a' => 'index' - )); + // 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'); + Minz_Request::good($content_notif); } private function guessFileType($filename) { @@ -120,7 +116,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } elseif (substr_compare($filename, '.opml', -5) === 0 || substr_compare($filename, '.xml', -4) === 0) { return 'opml'; - } elseif (strcmp($filename, 'starred.json') === 0) { + } elseif (substr_compare($filename, '.json', -5) === 0 && + strpos($filename, 'starred') !== false) { return 'json_starred'; } elseif (substr_compare($filename, '.json', -5) === 0 && strpos($filename, 'feed_') === 0) { @@ -176,15 +173,15 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } // We get different useful information - $url = html_chars_utf8($feed_elt['xmlUrl']); - $name = html_chars_utf8($feed_elt['text']); + $url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']); + $name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']); $website = ''; if (isset($feed_elt['htmlUrl'])) { - $website = html_chars_utf8($feed_elt['htmlUrl']); + $website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']); } $description = ''; if (isset($feed_elt['description'])) { - $description = html_chars_utf8($feed_elt['description']); + $description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']); } $error = false; @@ -210,7 +207,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { private function addCategoryOpml($cat_elt, $parent_cat) { // Create a new Category object - $cat = new FreshRSS_Category(html_chars_utf8($cat_elt['text'])); + $cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text'])); $id = $this->catDAO->addCategoryObject($cat); $error = ($id === false); @@ -287,7 +284,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $url = $origin[$key]; $name = $origin['title']; $website = $origin['htmlUrl']; - $error = false; + try { // Create a Feed object and add it in DB $feed = new FreshRSS_Feed($url); @@ -311,61 +308,53 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } public function exportAction() { - if (Minz_Request::isPost()) { - $this->view->_useLayout(false); + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); + } - $export_opml = Minz_Request::param('export_opml', false); - $export_starred = Minz_Request::param('export_starred', false); - $export_feeds = Minz_Request::param('export_feeds', array ()); + $this->view->_useLayout(false); - $export_files = array (); - if ($export_opml) { - $export_files['feeds.opml'] = $this->generateOpml(); - } + $export_opml = Minz_Request::param('export_opml', false); + $export_starred = Minz_Request::param('export_starred', false); + $export_feeds = Minz_Request::param('export_feeds', array()); - if ($export_starred) { - $export_files['starred.json'] = $this->generateArticles('starred'); - } + $export_files = array(); + if ($export_opml) { + $export_files['feeds.opml'] = $this->generateOpml(); + } - foreach ($export_feeds as $feed_id) { - $feed = $this->feedDAO->searchById($feed_id); - if ($feed) { - $filename = 'feed_' . $feed->category() . '_' - . $feed->id() . '.json'; - $export_files[$filename] = $this->generateArticles( - 'feed', $feed - ); - } - } + if ($export_starred) { + $export_files['starred.json'] = $this->generateArticles('starred'); + } - $nb_files = count($export_files); - if ($nb_files > 1) { - // If there are more than 1 file to export, we need an .zip - try { - $this->exportZip($export_files); - } catch (Exception $e) { - # Oops, there is no Zip extension! - $notif = array( - 'type' => 'bad', - 'content' => _t('export_no_zip_extension') - ); - Minz_Session::_param('notification', $notif); - Minz_Request::forward(array('c' => 'importExport'), true); - } - } elseif ($nb_files === 1) { - // Only one file? Guess its type and export it. - $filename = key($export_files); - $type = null; - if (substr_compare($filename, '.opml', -5) === 0) { - $type = "text/xml"; - } elseif (substr_compare($filename, '.json', -5) === 0) { - $type = "text/json"; - } + foreach ($export_feeds as $feed_id) { + $feed = $this->feedDAO->searchById($feed_id); + if ($feed) { + $filename = 'feed_' . $feed->category() . '_' + . $feed->id() . '.json'; + $export_files[$filename] = $this->generateArticles( + 'feed', $feed + ); + } + } - $this->exportFile($filename, $export_files[$filename], $type); - } else { - Minz_Request::forward(array('c' => 'importExport'), true); + $nb_files = count($export_files); + if ($nb_files > 1) { + // If there are more than 1 file to export, we need a zip archive. + try { + $this->exportZip($export_files); + } catch (Exception $e) { + # Oops, there is no Zip extension! + Minz_Request::bad(_t('export_no_zip_extension'), + array('c' => 'importExport', 'a' => 'index')); } + } elseif ($nb_files === 1) { + // Only one file? Guess its type and export it. + $filename = key($export_files); + $type = $this->guessFileType($filename); + $this->exportFile('freshrss_' . $filename, $export_files[$filename], $type); + } else { + Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); } } @@ -384,7 +373,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->view->categories = $this->catDAO->listCategories(); if ($type == 'starred') { - $this->view->list_title = Minz_Translate::t('starred_list'); + $this->view->list_title = _t('starred_list'); $this->view->type = 'starred'; $unread_fav = $this->entryDAO->countUnreadReadFavorites(); $this->view->entries = $this->entryDAO->listWhere( @@ -392,9 +381,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $unread_fav['all'] ); } elseif ($type == 'feed' && !is_null($feed)) { - $this->view->list_title = Minz_Translate::t( - 'feed_list', $feed->name() - ); + $this->view->list_title = _t('feed_list', $feed->name()); $this->view->type = 'feed/' . $feed->id(); $this->view->entries = $this->entryDAO->listWhere( 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', @@ -430,11 +417,18 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } private function exportFile($filename, $content, $type) { - if (is_null($type)) { + if ($type === 'unknown') { return; } - header('Content-Type: ' . $type . '; charset=utf-8'); + $content_type = ''; + if ($type === 'opml') { + $content_type = "text/opml"; + } elseif ($type === 'json_feed' || $type === 'json_starred') { + $content_type = "text/json"; + } + + header('Content-Type: ' . $content_type . '; charset=utf-8'); header('Content-disposition: attachment; filename=' . $filename); print($content); } diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 3119073b8..b0b051119 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -76,14 +76,14 @@ class FreshRSS_index_Controller extends Minz_ActionController { ); // On récupère les différents éléments de filtrage - $this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view); + $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', ''); - if ($state === FreshRSS_Entry::STATE_NOT_READ) { //Any unread article in this category at all? + 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; @@ -104,7 +104,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { break; } if (!$hasUnread && ($state_param === null)) { - $this->view->state = $state = FreshRSS_Entry::STATE_ALL; + $this->view->state = FreshRSS_Entry::STATE_ALL; } } @@ -117,11 +117,11 @@ class FreshRSS_index_Controller extends Minz_ActionController { $keepHistoryDefault = $this->view->conf->keep_history_default; try { - $entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault); + $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault); // Si on a récupéré aucun article "non lus" // on essaye de récupérer tous les articles - if ($state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) { + 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 { @@ -132,6 +132,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { $this->view->state = FreshRSS_Entry::STATE_ALL; $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault); } + Minz_Request::_param('state', $this->view->state); if (count($entries) <= $nb) { $this->view->nextId = ''; @@ -295,6 +296,41 @@ class FreshRSS_index_Controller extends Minz_ActionController { 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) { + if ($fileInfo->getExtension() === 'txt' && $fileInfo->getMTime() < $oldest) { + @unlink($fileInfo->getPathname()); + } + } + } + public function formLoginAction () { if (Minz_Request::isPost()) { $ok = false; @@ -312,6 +348,11 @@ class FreshRSS_index_Controller extends Minz_ActionController { 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); } @@ -371,6 +412,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { Minz_Session::_param('currentUser'); Minz_Session::_param('mail'); Minz_Session::_param('passwordHash'); + self::deleteLongTermCookie(); Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); } } diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 06a20c2a6..000b41dd2 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -58,15 +58,20 @@ class FreshRSS_stats_Controller extends Minz_ActionController { public function repartitionAction() { $statsDAO = FreshRSS_Factory::createStatsDAO(); + $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); + $this->view->categories = $categoryDAO->listCategories(); $this->view->feed = $feedDAO->searchById($id); $this->view->days = $statsDAO->getDays(); $this->view->months = $statsDAO->getMonths(); $this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id); + $this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id); $this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id); + $this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id); $this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id); + $this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id); } public function firstAction() { diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 7c333b090..cf6390f68 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -6,8 +6,7 @@ class FreshRSS extends Minz_FrontController { } $loginOk = $this->accessControl(Minz_Session::param('currentUser', '')); $this->loadParamsView(); - if (Minz_Request::isPost() && (empty($_SERVER['HTTP_REFERER']) || - Minz_Request::getDomainName() !== parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST))) { + if (Minz_Request::isPost() && !Minz_Request::isRefererFromSameDomain()) { $loginOk = false; //Basic protection against XSRF attacks Minz_Error::error( 403, @@ -20,13 +19,35 @@ class FreshRSS extends Minz_FrontController { $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 accessControl($currentUser) { if ($currentUser == '') { switch (Minz_Configuration::authType()) { case 'form': - $currentUser = Minz_Configuration::defaultUser(); - Minz_Session::_param('passwordHash'); - $loginOk = false; + $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(); diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 2f47312c0..3a408faa5 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -17,6 +17,7 @@ class FreshRSS_Configuration { '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, @@ -44,6 +45,8 @@ class FreshRSS_Configuration { 'load_more' => 'm', 'auto_share' => 's', 'focus_search' => 'a', + 'user_filter' => 'u', + 'help' => 'f1', ), 'topline_read' => true, 'topline_favorite' => true, @@ -60,6 +63,7 @@ class FreshRSS_Configuration { ); private $available_languages = array( + 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', ); @@ -142,6 +146,9 @@ class FreshRSS_Configuration { 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; } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index a9ffa138b..75a8aeba4 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -65,7 +65,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } if (!isset($existingGuids[$entry->guid()]) && - ($feedHistory != 0 || $eDate >= $date_min)) { + ($feedHistory != 0 || $eDate >= $date_min || $entry->isFavorite())) { $values = $entry->toArray(); $useDeclaredDate = empty($existingGuids); @@ -230,7 +230,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } $this->bd->beginTransaction(); - $sql = 'UPDATE `' . $this->prefix . 'entry` ' + $sql = 'UPDATE `' . $this->prefix . 'entry` ' . 'SET is_read=1 ' . 'WHERE id_feed=? AND is_read=0 AND id <= ?'; $values = array($id, $idMax); diff --git a/app/Models/Feed.php b/app/Models/Feed.php index fe1e52ea2..2a5ea45ac 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -28,6 +28,12 @@ class FreshRSS_Feed extends Minz_Model { } } + public static function example() { + $f = new FreshRSS_Feed('http://example.net/', false); + $f->faviconPrepare(); + return $f; + } + public function id() { return $this->id; } diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php index 89be76a26..bd4271ba8 100644 --- a/app/Models/StatsDAO.php +++ b/app/Models/StatsDAO.php @@ -152,6 +152,68 @@ SQL; } /** + * Calculates the average number of article per hour per feed + * + * @param integer $feed id + * @return integer + */ + public function calculateEntryAveragePerFeedPerHour($feed = null) { + return $this->calculateEntryAveragePerFeedPerPeriod(1/24, $feed); + } + + /** + * Calculates the average number of article per day of week per feed + * + * @param integer $feed id + * @return integer + */ + public function calculateEntryAveragePerFeedPerDayOfWeek($feed = null) { + return $this->calculateEntryAveragePerFeedPerPeriod(7, $feed); + } + + /** + * Calculates the average number of article per month per feed + * + * @param integer $feed id + * @return integer + */ + public function calculateEntryAveragePerFeedPerMonth($feed = null) { + return $this->calculateEntryAveragePerFeedPerPeriod(30, $feed); + } + + /** + * Calculates the average number of article per feed + * + * @param float $period number used to divide the number of day in the period + * @param integer $feed id + * @return integer + */ + protected function calculateEntryAveragePerFeedPerPeriod($period, $feed = null) { + if ($feed) { + $restrict = "WHERE e.id_feed = {$feed}"; + } else { + $restrict = ''; + } + $sql = <<<SQL +SELECT COUNT(1) AS count +, MIN(date) AS date_min +, MAX(date) AS date_max +FROM {$this->prefix}entry AS e +{$restrict} +SQL; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetch(PDO::FETCH_NAMED); + $date_min = new \DateTime(); + $date_min->setTimestamp($res['date_min']); + $date_max = new \DateTime(); + $date_max->setTimestamp($res['date_max']); + $interval = $date_max->diff($date_min, true); + + return round($res['count'] / ($interval->format('%a') / ($period)), 2); + } + + /** * Initialize an array for statistics depending on a range * * @param integer $min diff --git a/app/i18n/de.php b/app/i18n/de.php new file mode 100644 index 000000000..4301a8b6d --- /dev/null +++ b/app/i18n/de.php @@ -0,0 +1,326 @@ +<?php + +return array ( + // LAYOUT + 'login' => 'Login', + 'login_with_persona' => 'Login mit Persona', + 'logout' => 'Logout', + 'search' => 'Suche nach Worten oder #tags', + 'search_short' => 'Suche', + + 'configuration' => 'Konfiguration', + 'users' => 'Nutzer', + 'categories' => 'Kategorien', + 'category' => 'Kategorie', + 'feed' => 'Feed', + 'feeds' => 'Feeds', + 'shortcuts' => 'Shortcuts', + 'about' => 'Über', + 'stats' => 'Statistiken', + + 'your_rss_feeds' => 'Ihre RSS Feeds', + 'add_rss_feed' => 'RSS-Feed hinzufügen', + 'no_rss_feed' => 'Kein RSS Feed', + 'import_export_opml' => 'Import / Export (OPML)', + + 'subscription_management' => 'Abonnementsverwaltung', + 'main_stream' => 'Haupt-Nachrichtenfluß', + 'all_feeds' => 'Alle Feeds', + 'favorite_feeds' => 'Favoriten (%d)', + 'not_read' => '%d ungelesen', + 'not_reads' => '%d ungelesen', + + 'filter' => 'Filter', + 'see_website' => 'Website ansehen', + 'administration' => 'Verwaltung', + 'actualize' => 'Aktualisierung', + + 'mark_read' => 'Als gelesen markieren', + 'mark_favorite' => 'Als Favoriten markieren', + 'mark_all_read' => 'Alle als gelesen markieren', + 'mark_feed_read' => 'Feed als gelesen markieren', + 'mark_cat_read' => 'Kategorie als gelesen markieren', + 'before_one_day' => 'Vor einem Tag', + 'before_one_week' => 'Vor einer Woche', + 'display' => 'Anzeige', + 'normal_view' => 'Normale Anzeige', + 'reader_view' => 'Leseanzeige-Modus', + 'global_view' => 'Globale Anzeige', + 'rss_view' => 'RSS-Feed', + 'show_all_articles' => 'zeige alle Artikel', + 'show_not_reads' => 'zeige nicht gelesene', + 'show_read' => 'zeige nur gelesene', + 'show_favorite' => 'Favoriten anzeigen', + 'older_first' => 'Älteste zuerst', + 'newer_first' => 'Neuere zuerst', + + // Pagination + 'first' => 'Erste', + 'previous' => 'Vorherige', + 'next' => 'Nächste', + 'last' => 'Letzte', + + // CONTROLLERS + 'article_published_on' => 'Dieser Artikel erschien im Original bei <a href="%s">%s</a>', + 'article_published_on_author' => 'Dieser Artikel erschien im Original bei <a href="%s">%s</a> von %s', + + 'access_denied' => 'Sie haben nicht die Berechtigung, diese Seite aufzurufen', + 'page_not_found' => 'Sie suchen nach einer Seite, die es nicht gibt', + 'error_occurred' => 'Es gab einen Fehler', + 'error_occurred_update' => 'Es wurde nichts geändert', + + 'default_category' => 'Unkategorisiert', + 'categories_updated' => 'Kategorien wurden aktualisiert', + 'categories_management' => 'Kategorienverwaltung', + 'feed_updated' => 'Der Feed wurde aktualisiert', + 'rss_feed_management' => 'Verwaltung der RSS Feeds', + 'configuration_updated' => 'Die Konfiguration wurde aktualisiert', + 'sharing_management' => 'Verwaltung der Optionen für das Teilen', + 'bad_opml_file' => 'Ihre OPML-Datei ist ungültig', + 'shortcuts_updated' => 'Shortcuts wurden aktualisiert', + 'shortcuts_management' => 'Verwaltung der Shortcuts', + 'shortcuts_navigation' => 'Navigation', + 'shortcuts_navigation_help' => 'Mit der "Shift" Taste gelten die Navigations-Shortcuts für Feeds.<br/>Mit der "Alt" Taste gelten die Navigations-Shortcuts für Kategorien.', + 'shortcuts_article_action' => 'Artikelaktionen', + 'shortcuts_other_action' => 'Andere Aktionen', + 'feeds_marked_read' => 'Die Feeds wurden als gelesen markiert', + 'updated' => 'Die Änderungen wurden aktualisiert', + + 'already_subscribed' => 'Sie haben bereits <em>%s</em> abonniert', + 'feed_added' => 'Der RSS Feed <em>%s</em> wurde hinzugefügt', + 'feed_not_added' => '<em>%s</em> konnte nicht hinzugefügt werden', + 'internal_problem_feed' => 'Der RSS Feed konnte nicht hinzugefügt werden. überprüfen Sie die Protokolldateien von FressRSS für weitere Informationen.', + 'invalid_url' => 'URL <em>%s</em> ist ungültig', + 'feed_actualized' => '<em>%s</em> wurde aktualisiert', + 'n_feeds_actualized' => '%d Feeds wurden aktualisiert', + 'feeds_actualized' => 'RSS Feeds wurden aktualisiert', + 'no_feed_actualized' => 'Es wurden keine RSS Feeds aktualisiert', + 'n_entries_deleted' => '%d Artikel wurden gelöscht', + 'feeds_imported_with_errors' => 'Ihre Feeds wurden importiert, es gab aber einige Fehler', + 'feeds_imported' => 'Ihre Feeds wurden importiert und werden jetzt aktualisiert', + 'category_emptied' => 'Die Kategorie wurde geleert', + 'feed_deleted' => 'Der Feed wurde gelöscht', + 'feed_validator' => '&Üuml;berprüfen Sie die Gültigkeit des Feeds', + + 'optimization_complete' => 'Die Optimierung ist beendet', + + 'your_rss_feeds' => 'Ihre RSS Feeds', + 'your_favorites' => 'Ihre Favoriten', + 'public' => 'Öffentlich', + 'invalid_login' => 'Das Login ist ungültig', + + // VIEWS + 'save' => 'Speichern', + 'delete' => 'Löschen', + 'cancel' => 'Abbrechen', + + 'back_to_rss_feeds' => '← Zurück zu den RSS Feeds gehen', + 'feeds_moved_category_deleted' => 'Wenn Sie eine Kategorie löschen, werden deren Feeds automatisch in die Kategorie <em>%s</em> eingefügt.', + 'category_number' => 'Kategorie n°%d', + 'ask_empty' => 'Leeren?', + 'number_feeds' => '%d Feeds', + 'can_not_be_deleted' => 'Kann nicht gelöscht werden', + 'add_category' => 'Füge eine Kategorie hinzu', + 'new_category' => 'Neue Kategorie', + + 'javascript_for_shortcuts' => 'JavaScript muss ermöglicht werden, wenn Shortcuts verwendet werden sollen', + 'javascript_should_be_activated'=> 'JavaScript muss ermöglicht werden', + 'shift_for_all_read' => '+ <code>shift</code> um alle Artikel als gelesen zu markieren', + 'see_on_website' => 'Auf der Originalwebseite anschauen', + 'next_article' => 'Zum nächsten Artikel springen', + 'last_article' => 'Zum letzten Artikel springen', + 'previous_article' => 'Zum vorherigen Artikel springen', + 'first_article' => 'Zum ersten Artikel springen', + 'next_page' => 'Zur nächsten Seite springen', + 'previous_page' => 'Zur vorherigen Seite springen', + 'collapse_article' => 'Zusammenfalten', + 'auto_share' => 'Teilen', + 'auto_share_help' => 'Wenn es nur eine Option zum Teilen gibt, wird die verwendet. Ansonsten werden die Optionen über die Nummer ausgewählt.', + + 'file_to_import' => 'Datei zum importieren', + 'import' => 'Import', + 'export' => 'Export', + 'or' => 'oder', + + 'informations' => 'Information', + 'damn' => 'Verdammt!', + 'feed_in_error' => 'Dieser Feed hat ein Problem verursacht. Bitte stellen Sie sicher, dass er immer lesbar ist und aktualisieren Sie ihn dann.', + 'feed_empty' => 'Dieser Feed ist leer. Bitte stellen Sie sicher, dass er noch gepflegt wird.', + 'feed_description' => 'Beschreibung', + 'website_url' => 'Webseiten-Adresse URL', + 'feed_url' => 'Feed URL', + 'articles' => 'Artikel', + 'number_articles' => 'Anzahl der Artikel', + 'by_feed' => 'per Feed', + 'by_default' => 'Als Vorgabe', + 'keep_history' => 'Kleinste Anzahl der Artikel, die behalten werden', + 'categorize' => 'In einer Kategorie speichern', + 'truncate' => 'Alle Artikel löschen', + 'advanced' => 'Erweitert', + 'show_in_all_flux' => 'Im Hauptstrom anzeigen', + 'yes' => 'Ja', + 'no' => 'Nein', + 'css_path_on_website' => 'Pfad zur CSS-Datei des Artikels auf der Original Webseite', + 'retrieve_truncated_feeds' => 'Gekürzte RSS Feeds abrufen (Achtung, benötigt mehr Zeit!)', + 'http_authentication' => 'HTTP Authentifizierung', + 'http_username' => 'HTTP Nutzername', + 'http_password' => 'HTTP Passwort', + 'blank_to_disable' => 'Zum Ausschalten frei lassen', + 'not_yet_implemented' => 'Noch nicht implementiert', + 'access_protected_feeds' => 'Die Verbindung erlaubt Zugriff zu HTTP-geschützten RSS Feeds', + 'no_selected_feed' => 'Kein Feed ausgewählt.', + 'think_to_add' => '<a href="./?c=configure&a=feed">Sie können Feeds hinzufügen</a>.', + + 'current_user' => 'Aktuelle Nutzung', + 'default_user' => 'Nutzername des Standardnutzers <small>(maximal 16 Zeichen - alphanumerisch)</small>', + 'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>', + 'persona_connection_email' => 'Login E-Mail Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>', + 'allow_anonymous' => 'Anonymes lesen der Artikel des Standardnutzers (%s) wird erlaubt', + 'allow_anonymous_refresh' => 'Aktualisieren der Artikel wird anonymen Nutzern erlaubt', + 'auth_token' => 'Authentifizierungs-Token', + 'explain_token' => 'Erlaube den Zugriff auf die RSS-Ausgabe des Standardnutzers ohne Authentifizierung.<br /><kbd>%s?output=rss&token=%s</kbd>', + 'login_configuration' => 'Login', + 'is_admin' => 'ist Administrator', + 'auth_type' => 'Authentifizierungsmethode', + 'auth_none' => 'Keine (gefährlich)', + 'auth_form' => 'Webformular (traditionell, JavaScript wird benötigt)', + 'http_auth' => 'HTTP (mit HTTPS für erfahrene Nutzer)', + 'auth_persona' => 'Mozilla Persona (modern, JavaScript wird benötigt)', + 'users_list' => 'Liste der Nutzer', + 'create_user' => 'Neuen Nutzer erstellen', + 'username' => 'Nutzername', + 'password' => 'Passwort', + 'create' => 'Erstellen', + 'user_created' => 'Nutzer %s wurde erstellt', + 'user_deleted' => 'Nutzer %s wurde gelöscht', + + 'language' => 'Sprache', + 'month' => 'Monate', + 'archiving_configuration' => 'Archivieren', + 'delete_articles_every' => 'Entfernen von Artikeln nach', + 'purge_now' => 'Jetzt bereinigen', + 'purge_completed' => 'Die Bereinigung ist abgeschlossen (%d Artikel wurden gelöscht)', + 'archiving_configuration_help' => 'Es gibt weitere Optionen bei den Einstellungen der individuellen Nachrichtenströme', + 'reading_configuration' => 'Lesen', + 'articles_per_page' => 'Anzahl der Artikel pro Seite', + 'default_view' => 'Standard-Ansicht', + 'sort_order' => 'Sortierreihenfolge', + 'auto_load_more' => 'Die nächsten Artikel am Seitenende laden', + 'display_articles_unfolded' => 'Die Artikel als Standard zusammen gefaltet anzeigen', + 'after_onread' => 'Nach “als gelesen markieren”', + 'jump_next' => 'springe zum nächsten ungelesenen Geschwisterelement (Feed oder Kategorie)', + 'reading_icons' => 'Lese Symbol', + 'top_line' => 'Kopfzeile', + 'bottom_line' => 'Fusszeile', + 'img_with_lazyload' => 'Verwende die "träge laden" Methode zum laden von Bildern', + 'auto_read_when' => 'Artikel als gelesen markieren…', + 'article_selected' => 'wenn der Artikel ausgewählt ist', + 'article_open_on_website' => 'wenn der Artikel auf der Originalwebseite geöffnet ist', + 'scroll' => 'während des Seiten-Scrollens', + 'upon_reception' => 'beim Empfang des Artikels', + 'your_shaarli' => 'Ihr Shaarli', + 'your_wallabag' => 'Ihr wallabag', + 'your_diaspora_pod' => 'Ihr Diaspora* pod', + 'sharing' => 'Teilen', + 'share' => 'teile', + 'by_email' => 'Per E-Mail', + 'optimize_bdd' => 'Datenbank optimieren', + 'optimize_todo_sometimes' => 'Sollte gelegentlich gemacht werden, um die Größe der Datenbank zu reduzieren', + 'theme' => 'Thema', + 'more_information' => 'Weitere Informationen', + 'activate_sharing' => 'Teilen aktivieren', + 'shaarli' => 'Shaarli', + 'wallabag' => 'wallabag', + 'diaspora' => 'Diaspora*', + 'twitter' => 'Twitter', + 'g+' => 'Google+', + 'facebook' => 'Facebook', + 'email' => 'E-Mail', + 'print' => 'Drucken', + + 'article' => 'Artikel', + 'title' => 'Titel', + 'author' => 'Autor', + 'publication_date' => 'Datum der Veröffentlichung', + 'by' => 'von', + + 'load_more' => 'Weitere Artikel laden', + 'nothing_to_load' => 'Es gibt keine weiteren Artikel', + + 'rss_feeds_of' => 'RSS Feed von %s', + + 'refresh' => 'Aktualisieren', + 'no_feed_to_refresh' => 'Es gibt keinen Feed zum aktualisieren', + + 'today' => 'Heute', + 'yesterday' => 'Gestern', + 'before_yesterday' => 'vor Gestern', + 'new_article' => 'Es gibt neue Artikel. Bitte klicken Sie hier, um die Seite erneut zu laden.', + 'by_author' => 'Von <em>%s</em>', + 'related_tags' => 'Verwandte tags', + 'no_feed_to_display' => 'Es gibt keinen Artikel zum anzeigen.', + + 'about_freshrss' => 'Über FreshRSS', + 'project_website' => 'Projekt Webseite', + 'lead_developer' => 'Hauptentwickler', + 'website' => 'Webseite', + 'bugs_reports' => 'Fehlerberichte', + 'github_or_email' => '<a href="https://github.com/marienfressinaud/FreshRSS/issues">auf Github</a> oder <a href="mailto:dev@marienfressinaud.fr">per Mail</a>', + 'license' => 'Lizenz', + 'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>', + 'freshrss_description' => 'FreshRSS ist ein RSS Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="http://projet.idleman.fr/leed/">Leed</a>. Es ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstark und konfigurierbares Werkzeug.', + 'credits' => 'Credits', + 'credits_content' => 'Einige Designelemente sind von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> sind vom <a href="https://www.gnome.org/">GNOME Projekt</a>. <em>Open Sans</em> Font police wurde von <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson erstellt</a>. Favicons wurden mit <a href="https://getfavicon.appspot.com/">getFavicon API gesammelt</a>. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP Framework.', + 'version' => 'Version', + + 'logs' => 'Protokolle', + 'logs_empty' => 'Die Protokolldatei ist leer', + 'clear_logs' => 'Protokolldateien leeren', + + 'forbidden_access' => 'Der Zugriff ist verboten!', + 'login_required' => 'Das Login ist nötig:', + + 'confirm_action' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Die Aktion kann nicht abgebrochen werden!', + + // DATE + 'january' => 'januar', + 'february' => 'februar', + 'march' => 'märz', + 'april' => 'april', + 'may' => 'mai', + 'june' => 'juni', + 'july' => 'juli', + 'august' => 'august', + 'september' => 'september', + 'october' => 'oktober', + 'november' => 'november', + 'december' => 'dezember', + // special format for date() function + 'Jan' => '\J\a\n\u\a\r', + 'Feb' => '\F\e\b\r\u\a\r', + 'Mar' => '\M\a\e\r\z', + 'Apr' => '\A\p\r\i\l', + 'May' => '\M\a\i', + 'Jun' => '\J\u\n\i', + 'Jul' => '\J\u\l\i', + 'Aug' => '\A\u\g\u\s\t', + 'Sep' => '\S\e\p\t\e\m\b\e\r', + 'Oct' => '\O\k\t\o\b\e\r', + 'Nov' => '\N\o\v\e\m\b\e\r', + 'Dec' => '\D\e\z\e\m\b\e\r', + // format for date() function, %s allows to indicate month in letter + 'format_date' => 'd\.\ %s Y', + 'format_date_hour' => 'd\.\ %s Y \u\m H\:i', + + 'status_favorites' => 'Favoriten', + 'status_read' => 'Gelesen', + 'status_unread' => 'Ungelesen', + 'status_total' => 'Gesamt', + + 'stats_entry_repartition' => 'Verteilung der Einträge', + 'stats_entry_per_day' => 'Einträge pro Tag (während der letzten 30 Tage)', + 'stats_feed_per_category' => 'Feeds pro Kategorie', + 'stats_entry_per_category' => 'Einträge pro Kategorie', + 'stats_top_feed' => 'Top 10 Feeds', + 'stats_entry_count' => 'Zähler für Einträge', +); diff --git a/app/i18n/en.php b/app/i18n/en.php index 6a0b4a139..95356af2c 100644 --- a/app/i18n/en.php +++ b/app/i18n/en.php @@ -3,6 +3,7 @@ return array ( // LAYOUT 'login' => 'Login', + 'keep_logged_in' => 'Keep me logged in <small>(1 month)</small>', 'login_with_persona' => 'Login with Persona', 'logout' => 'Logout', 'search' => 'Search words or #tags', @@ -179,9 +180,16 @@ return array ( '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<br />(OPML, Json or Zip)', + 'file_to_import_no_zip' => 'File to import<br />(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', @@ -263,6 +271,7 @@ return array ( '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 (only in “unread articles” display mode)', 'after_onread' => 'After “mark all as read”,', 'jump_next' => 'jump to next unread sibling (feed or category)', diff --git a/app/i18n/fr.php b/app/i18n/fr.php index d0637b9f7..8437e872e 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -3,6 +3,7 @@ return array ( // LAYOUT 'login' => 'Connexion', + 'keep_logged_in' => 'Rester connecté <small>(1 mois)</small>', 'login_with_persona' => 'Connexion avec Persona', 'logout' => 'Déconnexion', 'search' => 'Rechercher des mots ou des #tags', @@ -179,9 +180,16 @@ return array ( '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<br />(OPML, Json ou Zip)', + 'file_to_import_no_zip' => 'Fichier à importer<br />(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', @@ -263,6 +271,7 @@ return array ( '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 (uniquement en affichage “articles non lus”)', 'after_onread' => 'Après “marquer tout comme lu”,', 'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)', diff --git a/app/layout/aside_flux.phtml b/app/layout/aside_flux.phtml index aee8f8754..357aa1cd3 100644 --- a/app/layout/aside_flux.phtml +++ b/app/layout/aside_flux.phtml @@ -42,15 +42,19 @@ $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; + } } ?><li data-unread="<?php echo $cat->nbNotRead(); ?>"<?php if ($c_active) echo ' class="active"'; ?>><?php ?><div class="category stick<?php echo $c_active ? ' active' : ''; ?>"><?php ?><a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id(); echo Minz_Url::display($arUrl); ?>"><?php echo $cat->name (); ?></a><?php - ?><a class="btn dropdown-toggle" href="#"><?php echo FreshRSS_Themes::icon($c_active ? 'up' : 'down'); ?></a><?php + ?><a class="btn dropdown-toggle" href="#"><?php echo FreshRSS_Themes::icon($c_show ? 'up' : 'down'); ?></a><?php ?></div><?php - ?><ul class="feeds<?php echo $c_active ? ' active' : ''; ?>"><?php + ?><ul class="feeds<?php echo $c_show ? ' active' : ''; ?>"><?php foreach ($feeds as $feed) { $feed_id = $feed->id (); $nbEntries = $feed->nbEntries (); diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index 73a921c5d..7cd15c1a3 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -96,7 +96,7 @@ <li class="dropdown-header"><?php echo Minz_Translate::t('queries'); ?> <a class="no-mobile" href="<?php echo _url('configure', 'queries'); ?>"><?php echo FreshRSS_Themes::icon('configure'); ?></a></li> <?php foreach ($this->conf->queries as $query) { ?> - <li class="item"> + <li class="item query"> <a href="<?php echo $query['url']; ?>"><?php echo $query['name']; ?></a> </li> <?php } ?> @@ -164,11 +164,15 @@ break; } } - if ($this->order === 'ASC') { - $idMax = 0; - } else { - $p = isset($this->entries[0]) ? $this->entries[0] : null; - $idMax = $p === null ? '0' : $p->id(); + + $p = isset($this->entries[0]) ? $this->entries[0] : null; + $idMax = $p === null ? (time() - 1) . '000000' : $p->id(); + + if ($this->order === 'ASC') { //In this case we do not know but we guess idMax + $idMax2 = (time() - 1) . '000000'; + if (strcmp($idMax2, $idMax) > 0) { + $idMax = $idMax2; + } } $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('get' => $get, 'nextGet' => $nextGet, 'idMax' => $idMax)); diff --git a/app/views/configure/categorize.phtml b/app/views/configure/categorize.phtml index 9bae99b39..2f0e554ca 100644 --- a/app/views/configure/categorize.phtml +++ b/app/views/configure/categorize.phtml @@ -18,6 +18,9 @@ <input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" /> <?php if ($cat->nbFeed () > 0) { ?> + <a class="btn" href="<?php echo _url('index', 'index', 'get', 'c_' . $cat->id ()); ?>"> + <?php echo _i('link'); ?> + </a> <button type="submit" class="btn btn-attention confirm" formaction="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></button> <?php } ?> </div> diff --git a/app/views/configure/reading.phtml b/app/views/configure/reading.phtml index e96bcea42..5a26501a4 100644 --- a/app/views/configure/reading.phtml +++ b/app/views/configure/reading.phtml @@ -63,6 +63,16 @@ <div class="form-group"> <div class="group-controls"> + <label class="checkbox" for="display_categories"> + <input type="checkbox" name="display_categories" id="display_categories" value="1"<?php echo $this->conf->display_categories ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('display_categories_unfolded'); ?> + <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> + </label> + </div> + </div> + + <div class="form-group"> + <div class="group-controls"> <label class="checkbox" for="sticky_post"> <input type="checkbox" name="sticky_post" id="sticky_post" value="1"<?php echo $this->conf->sticky_post ? ' checked="checked"' : ''; ?> /> <?php echo Minz_Translate::t ('sticky_post'); ?> diff --git a/app/views/configure/shortcut.phtml b/app/views/configure/shortcut.phtml index bfb13f003..a4029b676 100644 --- a/app/views/configure/shortcut.phtml +++ b/app/views/configure/shortcut.phtml @@ -103,6 +103,21 @@ </div> </div> + <div class="form-group"> + <label class="group-name" for="user_filter_shortcut"><?php echo Minz_Translate::t ('user_filter'); ?></label> + <div class="group-controls"> + <input type="text" id="user_filter_shortcut" name="shortcuts[user_filter]" list="keys" value="<?php echo $s['user_filter']; ?>" /> + <?php echo Minz_Translate::t ('user_filter_help'); ?> + </div> + </div> + + <div class="form-group"> + <label class="group-name" for="help_shortcut"><?php echo Minz_Translate::t ('help'); ?></label> + <div class="group-controls"> + <input type="text" id="help_shortcut" name="shortcuts[help]" list="keys" value="<?php echo $s['help']; ?>" /> + </div> + </div> + <div class="form-group form-actions"> <div class="group-controls"> <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button> diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml index 7144c519a..2144f1576 100644 --- a/app/views/helpers/javascript_vars.phtml +++ b/app/views/helpers/javascript_vars.phtml @@ -4,7 +4,8 @@ echo '"use strict";', "\n"; $mark = $this->conf->mark_when; echo 'var ', - 'hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true', + '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', @@ -25,7 +26,9 @@ echo ',shortcuts={', 'collapse_entry:"', $s['collapse_entry'], '",', 'load_more:"', $s['load_more'], '",', 'auto_share:"', $s['auto_share'], '",', - 'focus_search:"', $s['focus_search'], '"', + 'focus_search:"', $s['focus_search'], '",', + 'user_filter:"', $s['user_filter'], '",', + 'help:"', $s['help'], '"', "},\n"; if (Minz_Request::param ('output') === 'global') { diff --git a/app/views/helpers/view/normal_view.phtml b/app/views/helpers/view/normal_view.phtml index 55ef6bdf6..87bf2e22a 100644 --- a/app/views/helpers/view/normal_view.phtml +++ b/app/views/helpers/view/normal_view.phtml @@ -81,7 +81,12 @@ if (!empty($this->entries)) { } } $feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed ()); //We most likely already have the feed object in cache - if (empty($feed)) $feed = $item->feed (true); + if ($feed == null) { + $feed = $item->feed(true); + if ($feed == null) { + $feed = FreshRSS_Feed::example(); + } + } ?><li class="item website"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span></a></li> <li class="item title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></li> <?php if ($topline_date) { ?><li class="item date"><?php echo $item->date (); ?> </li><?php } ?> diff --git a/app/views/importExport/index.phtml b/app/views/importExport/index.phtml index e1458e916..35371faca 100644 --- a/app/views/importExport/index.phtml +++ b/app/views/importExport/index.phtml @@ -6,7 +6,9 @@ <form method="post" action="<?php echo _url('importExport', 'import'); ?>" enctype="multipart/form-data"> <legend><?php echo _t('import'); ?></legend> <div class="form-group"> - <label class="group-name" for="file"><?php echo _t('file_to_import'); ?></label> + <label class="group-name" for="file"> + <?php echo extension_loaded('zip') ? _t('file_to_import') : _t('file_to_import_no_zip'); ?> + </label> <div class="group-controls"> <input type="file" name="file" id="file" /> </div> diff --git a/app/views/index/formLogin.phtml b/app/views/index/formLogin.phtml index cc925ea59..b79c1b614 100644 --- a/app/views/index/formLogin.phtml +++ b/app/views/index/formLogin.phtml @@ -1,32 +1,39 @@ <div class="prompt"> - <h1><?php echo Minz_Translate::t('login'); ?></h1><?php + <h1><?php echo _t('login'); ?></h1><?php switch (Minz_Configuration::authType()) { case 'form': ?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>"> <div> - <label for="username"><?php echo Minz_Translate::t('username'); ?></label> + <label for="username"><?php echo _t('username'); ?></label> <input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" /> </div> <div> - <label for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label> + <label for="passwordPlain"><?php echo _t('password'); ?></label> <input type="password" id="passwordPlain" required="required" /> <input type="hidden" id="challenge" name="challenge" /><br /> - <noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript> + <noscript><strong><?php echo _t('javascript_should_be_activated'); ?></strong></noscript> </div> <div> - <button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button> + <label class="checkbox" for="keep_logged_in"> + <input type="checkbox" name="keep_logged_in" id="keep_logged_in" value="1" /> + <?php echo _t('keep_logged_in'); ?> + </label> + <br /> + </div> + <div> + <button id="loginButton" type="submit" class="btn btn-important"><?php echo _t('login'); ?></button> </div> </form><?php break; case 'persona': ?><p> - <?php echo FreshRSS_Themes::icon('login'); ?> - <a class="signin" href="#"><?php echo Minz_Translate::t('login_with_persona'); ?></a> + <?php echo _i('login'); ?> + <a class="signin" href="#"><?php echo _t('login_with_persona'); ?></a> </p><?php break; } ?> - <p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p> + <p><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about_freshrss'); ?></a></p> </div> diff --git a/app/views/javascript/actualize.phtml b/app/views/javascript/actualize.phtml index d08dc47d1..74cef4998 100644 --- a/app/views/javascript/actualize.phtml +++ b/app/views/javascript/actualize.phtml @@ -1,25 +1,24 @@ "use strict"; -var feeds = [<?php - foreach ($this->feeds as $feed) { - echo "'", Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'), "',\n"; - } - ?>], +var feeds = [<?php foreach ($this->feeds as $feed) { ?>{<?php + ?>url: "<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'); ?>",<?php + ?>title: "<?php echo $feed->name(); ?>"<?php +?>},<?php } ?>], feed_processed = 0, feed_count = feeds.length; function initProgressBar(init) { if (init) { $("body").after("\<div id=\"actualizeProgress\" class=\"notification good\">\ - <?php echo _t('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\ - <progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feed_count + "\"></progress>\ + <?php echo _t('refresh'); ?><br /><span class=\"title\">/</span><br />\ + <span class=\"progress\">0 / " + feed_count + "</span>\ </div>"); } else { window.location.reload(); } } -function updateProgressBar(i) { - $("#actualizeProgressBar").val(i); +function updateProgressBar(i, title_feed) { $("#actualizeProgress .progress").html(i + " / " + feed_count); + $("#actualizeProgress .title").html(title_feed); } function updateFeeds() { @@ -43,10 +42,10 @@ function updateFeed() { $.ajax({ type: 'POST', - url: feed, + url: feed['url'], }).complete(function (data) { feed_processed++; - updateProgressBar(feed_processed); + updateProgressBar(feed_processed, feed['title']); if (feed_processed === feed_count) { initProgressBar(false); diff --git a/app/views/stats/repartition.phtml b/app/views/stats/repartition.phtml index 09892d3c5..d9dc4c89d 100644 --- a/app/views/stats/repartition.phtml +++ b/app/views/stats/repartition.phtml @@ -2,23 +2,38 @@ <div class="post content"> <a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a> - + + <h1><?php echo _t('stats_repartition'); ?></h1> + + <select id="feed_select"> + <option data-url="<?php echo _url('stats', 'repartition')?>"><?php echo _t('all_feeds')?></option> + <?php foreach ($this->categories as $category) { + $feeds = $category->feeds(); + if (!empty($feeds)) { + echo '<optgroup label="', $category->name(), '">'; + foreach ($feeds as $feed) { + if ($this->feed && $feed->id() == $this->feed->id()){ + echo '<option value="', $feed->id(), '" selected="selected" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>'; + } else { + echo '<option value="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>'; + } + } + echo '</optgroup>'; + } + }?> + </select> + <?php if ($this->feed) {?> - <h1> - <?php echo _t('stats_repartition'), " - "; ?> - <a href="<?php echo _url('configure', 'feed', 'id', $this->feed->id()); ?>"> - <?php echo $this->feed->name(); ?> - </a> - </h1> - <?php } else {?> - <h1><?php echo _t('stats_repartition'); ?></h1> + <a href="<?php echo _url('configure', 'feed', 'id', $this->feed->id()); ?>"> + <?php echo _t('administration'); ?> + </a> <?php }?> - + <div class="stat"> <h2><?php echo _t('stats_entry_per_hour'); ?></h2> <div id="statsEntryPerHour" style="height: 300px"></div> </div> - + <div class="stat"> <h2><?php echo _t('stats_entry_per_day_of_week'); ?></h2> <div id="statsEntryPerDayOfWeek" style="height: 300px"></div> @@ -41,11 +56,22 @@ function initStats() { return; } // Entry per hour + var avg_h = []; + for (var i = -1; i <= 24; i++) { + avg_h.push([i, <?php echo $this->averageHour?>]); + } Flotr.draw(document.getElementById('statsEntryPerHour'), - [<?php echo $this->repartitionHour ?>], + [{ + data: <?php echo $this->repartitionHour ?>, + bars: {horizontal: false, show: true} + }, { + data: avg_h, + lines: {show: true}, + label: <?php echo $this->averageHour?>, + yaxis: 2 + }], { grid: {verticalLines: false}, - bars: {horizontal: false, show: true}, xaxis: {noTicks: 23, tickFormatter: function(x) { var x = parseInt(x); @@ -55,14 +81,26 @@ 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, <?php echo $this->averageDayOfWeek?>]); + } Flotr.draw(document.getElementById('statsEntryPerDayOfWeek'), - [<?php echo $this->repartitionDayOfWeek ?>], + [{ + data: <?php echo $this->repartitionDayOfWeek ?>, + bars: {horizontal: false, show: true} + }, { + data: avg_dow, + lines: {show: true}, + label: <?php echo $this->averageDayOfWeek?>, + yaxis: 2 + }], { grid: {verticalLines: false}, - bars: {horizontal: false, show: true}, xaxis: {noTicks: 6, tickFormatter: function(x) { var x = parseInt(x), @@ -73,14 +111,26 @@ 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, <?php echo $this->averageMonth?>]); + } Flotr.draw(document.getElementById('statsEntryPerMonth'), - [<?php echo $this->repartitionMonth ?>], + [{ + data: <?php echo $this->repartitionMonth ?>, + bars: {horizontal: false, show: true} + }, { + data: avg_m, + lines: {show: true}, + label: <?php echo $this->averageMonth?>, + yaxis: 2 + }], { grid: {verticalLines: false}, - bars: {horizontal: false, show: true}, xaxis: {noTicks: 12, tickFormatter: function(x) { var x = parseInt(x), @@ -91,9 +141,10 @@ 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);}} }); - + } initStats(); </script> diff --git a/constants.php b/constants.php index ba9c508dc..4c515d121 100644 --- a/constants.php +++ b/constants.php @@ -2,6 +2,7 @@ define('FRESHRSS_VERSION', '0.8-dev'); define('FRESHRSS_WEBSITE', 'http://freshrss.org'); define('FRESHRSS_UPDATE_WEBSITE', 'https://update.freshrss.org?v=' . FRESHRSS_VERSION); +define('FRESHRSS_WIKI', 'http://doc.freshrss.org'); // PHP text output compression http://php.net/ob_gzhandler (better to do it at Web server level) define('PHP_COMPRESSION', false); diff --git a/data/tokens/.gitignore b/data/tokens/.gitignore new file mode 100644 index 000000000..2211df63d --- /dev/null +++ b/data/tokens/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/data/tokens/index.html b/data/tokens/index.html new file mode 100644 index 000000000..85faaa37e --- /dev/null +++ b/data/tokens/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB"> +<head> +<meta charset="UTF-8" /> +<meta http-equiv="Refresh" content="0; url=/" /> +<title>Redirection</title> +<meta name="robots" content="noindex" /> +</head> + +<body> +<p><a href="/">Redirection</a></p> +</body> +</html> diff --git a/lib/Minz/Helper.php b/lib/Minz/Helper.php index b058211d3..f4a547c4e 100644 --- a/lib/Minz/Helper.php +++ b/lib/Minz/Helper.php @@ -12,11 +12,22 @@ class Minz_Helper { * Annule les effets des magic_quotes pour une variable donnée * @param $var variable à traiter (tableau ou simple variable) */ - public static function stripslashes_r ($var) { - if (is_array ($var)){ - return array_map (array ('Helper', 'stripslashes_r'), $var); + public static function stripslashes_r($var) { + if (is_array($var)){ + return array_map(array('Minz_Helper', 'stripslashes_r'), $var); } else { return stripslashes($var); } } + + /** + * Wrapper for htmlspecialchars. + * Force UTf-8 value and can be used on array too. + */ + public static function htmlspecialchars_utf8($var) { + if (is_array($var)) { + return array_map(array('Minz_Helper', 'htmlspecialchars_utf8'), $var); + } + return htmlspecialchars($var, ENT_COMPAT, 'UTF-8'); + } } diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index 755784522..ec4e25a6b 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -10,7 +10,7 @@ class Minz_Request { private static $controller_name = ''; private static $action_name = ''; - private static $params = array (); + private static $params = array(); private static $default_controller_name = 'index'; private static $default_action_name = 'index'; @@ -18,59 +18,53 @@ class Minz_Request { /** * Getteurs */ - public static function controllerName () { + public static function controllerName() { return self::$controller_name; } - public static function actionName () { + public static function actionName() { return self::$action_name; } - public static function params () { + public static function params() { return self::$params; } - static function htmlspecialchars_utf8 ($p) { - if (is_array($p)) { - return array_map('self::htmlspecialchars_utf8', $p); - } - return htmlspecialchars($p, ENT_COMPAT, 'UTF-8'); - } - public static function param ($key, $default = false, $specialchars = false) { - if (isset (self::$params[$key])) { + public static function param($key, $default = false, $specialchars = false) { + if (isset(self::$params[$key])) { $p = self::$params[$key]; - if(is_object($p) || $specialchars) { + if (is_object($p) || $specialchars) { return $p; } else { - return self::htmlspecialchars_utf8($p); + return Minz_Helper::htmlspecialchars_utf8($p); } } else { return $default; } } - public static function defaultControllerName () { + public static function defaultControllerName() { return self::$default_controller_name; } - public static function defaultActionName () { + public static function defaultActionName() { return self::$default_action_name; } /** * Setteurs */ - public static function _controllerName ($controller_name) { + public static function _controllerName($controller_name) { self::$controller_name = $controller_name; } - public static function _actionName ($action_name) { + public static function _actionName($action_name) { self::$action_name = $action_name; } - public static function _params ($params) { + public static function _params($params) { if (!is_array($params)) { - $params = array ($params); + $params = array($params); } self::$params = $params; } - public static function _param ($key, $value = false) { + public static function _param($key, $value = false) { if ($value === false) { - unset (self::$params[$key]); + unset(self::$params[$key]); } else { self::$params[$key] = $value; } @@ -79,22 +73,36 @@ class Minz_Request { /** * Initialise la Request */ - public static function init () { - self::magicQuotesOff (); + public static function init() { + self::magicQuotesOff(); } /** * Retourn le nom de domaine du site */ - public static function getDomainName () { + public static function getDomainName() { return $_SERVER['HTTP_HOST']; } + public static function isRefererFromSameDomain() { + if (empty($_SERVER['HTTP_REFERER'])) { + return false; + } + $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']) { + return false; + } + return (isset($host['port']) ? $host['port'] : 0) === (isset($referer['port']) ? $referer['port'] : 0); + } + /** * Détermine la base de l'url * @return la base de l'url */ - public static function getBaseUrl () { + public static function getBaseUrl() { $defaultBaseUrl = Minz_Configuration::baseUrl(); if (!empty($defaultBaseUrl)) { return $defaultBaseUrl; @@ -109,13 +117,13 @@ class Minz_Request { * Récupère l'URI de la requête * @return l'URI */ - public static function getURI () { - if (isset ($_SERVER['REQUEST_URI'])) { - $base_url = self::getBaseUrl (); + public static function getURI() { + if (isset($_SERVER['REQUEST_URI'])) { + $base_url = self::getBaseUrl(); $uri = $_SERVER['REQUEST_URI']; - $len_base_url = strlen ($base_url); - $real_uri = substr ($uri, $len_base_url); + $len_base_url = strlen($base_url); + $real_uri = substr($uri, $len_base_url); } else { $real_uri = ''; } @@ -129,16 +137,16 @@ class Minz_Request { * @param $redirect si vrai, force la redirection http * > sinon, le dispatcher recharge en interne */ - public static function forward ($url = array (), $redirect = false) { - $url = Minz_Url::checkUrl ($url); + public static function forward($url = array(), $redirect = false) { + $url = Minz_Url::checkUrl($url); if ($redirect) { - header ('Location: ' . Minz_Url::display ($url, 'php')); - exit (); + header('Location: ' . Minz_Url::display($url, 'php')); + exit(); } else { - self::_controllerName ($url['c']); - self::_actionName ($url['a']); - self::_params (array_merge ( + self::_controllerName($url['c']); + self::_actionName($url['a']); + self::_params(array_merge( self::$params, $url['params'] )); @@ -146,6 +154,31 @@ class Minz_Request { } } + + /** + * Wrappers good notifications + redirection + * @param $msg notification content + * @param $url url array to where we should be forwarded + */ + public static function good($msg, $url = array()) { + Minz_Session::_param('notification', array( + 'type' => 'good', + 'content' => $msg + )); + + Minz_Request::forward($url, true); + } + + public static function bad($msg, $url = array()) { + Minz_Session::_param('notification', array( + 'type' => 'bad', + 'content' => $msg + )); + + Minz_Request::forward($url, true); + } + + /** * Permet de récupérer une variable de type $_GET * @param $param nom de la variable @@ -154,10 +187,10 @@ class Minz_Request { * $_GET si $param = false * $default si $_GET[$param] n'existe pas */ - public static function fetchGET ($param = false, $default = false) { + public static function fetchGET($param = false, $default = false) { if ($param === false) { return $_GET; - } elseif (isset ($_GET[$param])) { + } elseif (isset($_GET[$param])) { return $_GET[$param]; } else { return $default; @@ -172,10 +205,10 @@ class Minz_Request { * $_POST si $param = false * $default si $_POST[$param] n'existe pas */ - public static function fetchPOST ($param = false, $default = false) { + public static function fetchPOST($param = false, $default = false) { if ($param === false) { return $_POST; - } elseif (isset ($_POST[$param])) { + } elseif (isset($_POST[$param])) { return $_POST[$param]; } else { return $default; @@ -188,15 +221,16 @@ class Minz_Request { * $_POST * $_COOKIE */ - private static function magicQuotesOff () { - if (get_magic_quotes_gpc ()) { - $_GET = Minz_Helper::stripslashes_r ($_GET); - $_POST = Minz_Helper::stripslashes_r ($_POST); - $_COOKIE = Minz_Helper::stripslashes_r ($_COOKIE); + private static function magicQuotesOff() { + if (get_magic_quotes_gpc()) { + $_GET = Minz_Helper::stripslashes_r($_GET); + $_POST = Minz_Helper::stripslashes_r($_POST); + $_COOKIE = Minz_Helper::stripslashes_r($_COOKIE); } } - public static function isPost () { - return $_SERVER['REQUEST_METHOD'] === 'POST'; + public static function isPost() { + return isset($_SERVER['REQUEST_METHOD']) && + $_SERVER['REQUEST_METHOD'] === 'POST'; } } diff --git a/lib/Minz/Session.php b/lib/Minz/Session.php index ddabc4658..af4de75bb 100644 --- a/lib/Minz/Session.php +++ b/lib/Minz/Session.php @@ -2,28 +2,20 @@ /** * La classe Session gère la session utilisateur - * C'est un singleton */ class Minz_Session { /** - * $session stocke les variables de session - */ - private static $session = array (); //TODO: Try to avoid having another local copy - - /** * Initialise la session, avec un nom - * Le nom de session est utilisé comme nom pour les cookies et les URLs (i.e. PHPSESSID). + * Le nom de session est utilisé comme nom pour les cookies et les URLs(i.e. PHPSESSID). * Il ne doit contenir que des caractères alphanumériques ; il doit être court et descriptif */ - public static function init ($name) { - // démarre la session - session_name ($name); - session_set_cookie_params (0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true); - session_start (); + public static function init($name) { + $cookie = session_get_cookie_params(); + self::keepCookie($cookie['lifetime']); - if (isset ($_SESSION)) { - self::$session = $_SESSION; - } + // démarre la session + session_name($name); + session_start(); } @@ -32,8 +24,8 @@ class Minz_Session { * @param $p le paramètre à récupérer * @return la valeur de la variable de session, false si n'existe pas */ - public static function param ($p, $default = false) { - return isset(self::$session[$p]) ? self::$session[$p] : $default; + public static function param($p, $default = false) { + return isset($_SESSION[$p]) ? $_SESSION[$p] : $default; } @@ -42,13 +34,11 @@ class Minz_Session { * @param $p le paramètre à créer ou modifier * @param $v la valeur à attribuer, false pour supprimer */ - public static function _param ($p, $v = false) { + public static function _param($p, $v = false) { if ($v === false) { - unset ($_SESSION[$p]); - unset (self::$session[$p]); + unset($_SESSION[$p]); } else { $_SESSION[$p] = $v; - self::$session[$p] = $v; } } @@ -57,15 +47,47 @@ class Minz_Session { * Permet d'effacer une session * @param $force si à false, n'efface pas le paramètre de langue */ - public static function unset_session ($force = false) { - $language = self::param ('language'); + public static function unset_session($force = false) { + $language = self::param('language'); session_destroy(); - self::$session = array (); + $_SESSION = array(); if (!$force) { - self::_param ('language', $language); - Minz_Translate::reset (); + self::_param('language', $language); + Minz_Translate::reset(); } } + + + /** + * Spécifie la durée de vie des cookies + * @param $l la durée de vie + */ + public static function keepCookie($l) { + $cookie_dir = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI']; + session_set_cookie_params($l, $cookie_dir, '', false, true); + } + + + /** + * Régénère un id de session. + * Utile pour appeler session_set_cookie_params après session_start() + */ + public static function regenerateID() { + session_regenerate_id(true); + } + + public static function deleteLongTermCookie($name) { + setcookie($name, '', 1, '', '', false, true); + } + + public static function setLongTermCookie($name, $value, $expire) { + setcookie($name, $value, $expire, '', '', false, true); + } + + public static function getLongTermCookie($name) { + return isset($_COOKIE[$name]) ? $_COOKIE[$name] : null; + } + } diff --git a/lib/SimplePie/SimplePie/Parser.php b/lib/SimplePie/SimplePie/Parser.php index 9300b4ba9..7fb7bd9be 100644 --- a/lib/SimplePie/SimplePie/Parser.php +++ b/lib/SimplePie/SimplePie/Parser.php @@ -142,7 +142,7 @@ class SimplePie_Parser $dom = new DOMDocument(); $dom->recover = true; $dom->strictErrorChecking = false; - $dom->loadXML($data); + @$dom->loadXML($data); $this->encoding = $encoding = $dom->encoding = 'UTF-8'; $data2 = $dom->saveXML(); if (function_exists('mb_convert_encoding')) diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 86c0a4ae4..823f53716 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -230,7 +230,3 @@ function cryptAvailable() { } return false; } - -function html_chars_utf8($str) { - return htmlspecialchars($str, ENT_COMPAT, 'UTF-8'); -} diff --git a/p/api/greader.php b/p/api/greader.php index 7a961225f..5a6fdad7d 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -135,6 +135,7 @@ function checkCompatibility() { } if ((!array_key_exists('HTTP_AUTHORIZATION', $_SERVER)) && //Apache mod_rewrite trick should be fine (empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') === false)) && //nginx should be fine + (empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') === false)) && //lighttpd should be fine ((!function_exists('getallheaders')) || (stripos(php_sapi_name(), 'cgi') !== false))) { //Main problem is Apache/CGI mode die('FAIL getallheaders! (probably)'); } diff --git a/p/i/.gitignore b/p/i/.gitignore new file mode 100644 index 000000000..03c88fd7a --- /dev/null +++ b/p/i/.gitignore @@ -0,0 +1 @@ +.htaccess diff --git a/p/scripts/main.js b/p/scripts/main.js index ae7b69364..2108eece2 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -297,7 +297,7 @@ function next_entry() { function prev_feed() { var active_feed = $("#aside_flux .feeds li.active"); if (active_feed.length > 0) { - active_feed.prev().find('a.feed').each(function(){this.click();}); + active_feed.prevAll(':visible:first').find('a.feed').each(function(){this.click();}); } else { last_feed(); } @@ -306,21 +306,21 @@ function prev_feed() { function next_feed() { var active_feed = $("#aside_flux .feeds li.active"); if (active_feed.length > 0) { - active_feed.next().find('a.feed').each(function(){this.click();}); + active_feed.nextAll(':visible:first').find('a.feed').each(function(){this.click();}); } else { first_feed(); } } function first_feed() { - var feed = $("#aside_flux .feeds.active li:first"); + var feed = $("#aside_flux .feeds.active li:visible:first"); if (feed.length > 0) { feed.find('a')[1].click(); } } function last_feed() { - var feed = $("#aside_flux .feeds.active li:last"); + var feed = $("#aside_flux .feeds.active li:visible:last"); if (feed.length > 0) { feed.find('a')[1].click(); } @@ -330,7 +330,7 @@ function prev_category() { var active_cat = $("#aside_flux .category.stick.active"); if (active_cat.length > 0) { - var prev_cat = active_cat.parent('li').prev().find('.category.stick a.btn'); + var prev_cat = active_cat.parent('li').prevAll(':visible:first').find('.category.stick a.btn'); if (prev_cat.length > 0) { prev_cat[0].click(); } @@ -344,7 +344,7 @@ function next_category() { var active_cat = $("#aside_flux .category.stick.active"); if (active_cat.length > 0) { - var next_cat = active_cat.parent('li').next().find('.category.stick a.btn'); + var next_cat = active_cat.parent('li').nextAll(':visible:first').find('.category.stick a.btn'); if (next_cat.length > 0) { next_cat[0].click(); } @@ -355,14 +355,14 @@ function next_category() { } function first_category() { - var cat = $("#aside_flux .category.stick:first"); + var cat = $("#aside_flux .category.stick:visible:first"); if (cat.length > 0) { cat.find('a.btn')[0].click(); } } function last_category() { - var cat = $("#aside_flux .category.stick:last"); + var cat = $("#aside_flux .category.stick:visible:last"); if (cat.length > 0) { cat.find('a.btn')[0].click(); } @@ -373,11 +373,41 @@ function collapse_entry() { var flux_current = $(".flux.current"); flux_current.toggleClass("active"); - if (isCollapsed) { + if (isCollapsed && auto_mark_article) { mark_read(flux_current, true); } } +function user_filter(key) { + console.log('user filter'); + console.warn(key); + var filter = $('#dropdown-query'); + var filters = filter.siblings('.dropdown-menu').find('.item.query a'); + if (typeof key === "undefined") { + if (!filter.length) { + return; + } + // Display the filter div + window.location.hash = filter.attr('id'); + // Force scrolling to the filter div + var scroll = needsScroll($('.header')); + if (scroll !== 0) { + $('html,body').scrollTop(scroll); + } + // Force the key value if there is only one action, so we can trigger it automatically + if (filters.length === 1) { + key = 1; + } else { + return; + } + } + // Trigger selected share action + key = parseInt(key); + if (key <= filters.length) { + filters[key - 1].click(); + } +} + function auto_share(key) { var share = $(".flux.current.active").find('.dropdown-target[id^="dropdown-share"]'); var shares = share.siblings('.dropdown-menu').find('.item a'); @@ -531,9 +561,19 @@ function init_shortcuts() { }, { 'disable_in_input': true }); + + shortcut.add(shortcuts.user_filter, function () { + user_filter(); + }, { + 'disable_in_input': true + }); for(var i = 1; i < 10; i++){ shortcut.add(i.toString(), function (e) { - auto_share(String.fromCharCode(e.keyCode)); + if ($('#dropdown-query').siblings('.dropdown-menu').is(':visible')) { + user_filter(String.fromCharCode(e.keyCode)); + } else { + auto_share(String.fromCharCode(e.keyCode)); + } }, { 'disable_in_input': true }); @@ -618,6 +658,13 @@ function init_shortcuts() { }, { 'disable_in_input': true }); + + shortcut.add(shortcuts.help, function () { + redirect(help_url, true); + }, { + 'disable_in_input': true + }); + } function init_stream(divStream) { @@ -663,7 +710,7 @@ function init_stream(divStream) { if (auto_mark_site) { divStream.on('click', '.flux .link > a', function () { - mark_read($(this).parent().parent().parent(), true); + mark_read($(this).parents(".flux"), true); }); } } @@ -1063,6 +1110,12 @@ function init_share_observers() { }); } +function init_stats_observers() { + $('#feed_select').on('change', function(e) { + redirect($(this).find(':selected').data('url')); + }); +} + function init_remove_observers() { $('.post').on('click', 'a.remove', function(e) { var remove_what = $(this).attr('data-remove'); @@ -1177,6 +1230,7 @@ function init_all() { init_remove_observers(); init_feed_observers(); init_password_observers(); + init_stats_observers(); } if (window.console) { diff --git a/p/themes/Dark/dark.css b/p/themes/Dark/dark.css index 5edc09042..2ef48c406 100644 --- a/p/themes/Dark/dark.css +++ b/p/themes/Dark/dark.css @@ -515,15 +515,13 @@ a.btn { .categories .feeds .item.empty.active { background: #c95; } -.categories .feeds .item.empty.active .feed { - color: #fff; -} .categories .feeds .item.error .feed { color: #a44; } .categories .feeds .item.error.active { background: #a44; } +.categories .feeds .item.empty.active .feed, .categories .feeds .item.error.active .feed { color: #fff; } @@ -570,7 +568,7 @@ a.btn { } .prompt form { margin: 10px auto 20px auto; - width: 180px; + width: 200px; } .prompt input { margin: 5px auto; diff --git a/p/themes/Flat/flat.css b/p/themes/Flat/flat.css index 4ae3e98e9..fcfbb1424 100644 --- a/p/themes/Flat/flat.css +++ b/p/themes/Flat/flat.css @@ -492,10 +492,6 @@ a.btn { .categories .feeds .item.active { background: #2980b9; } -.categories .feeds .item.active .feed, -.categories .feeds .item.empty.active .feed { - color: #fff; -} .categories .feeds .item.empty.active { background: #f39c12; } @@ -508,6 +504,11 @@ a.btn { .categories .feeds .item.error .feed { color: #bd362f; } +.categories .feeds .item.active .feed, +.categories .feeds .item.empty.active .feed, +.categories .feeds .item.error.active .feed { + color: #fff; +} .categories .feeds .item .feed { margin: 0; width: 165px; @@ -551,7 +552,7 @@ a.btn { } .prompt form { margin: 10px auto 20px auto; - width: 180px; + width: 200px; } .prompt input { margin: 5px auto; diff --git a/p/themes/Origine/origine.css b/p/themes/Origine/origine.css index 0b95e2d70..0d1d95bad 100644 --- a/p/themes/Origine/origine.css +++ b/p/themes/Origine/origine.css @@ -540,21 +540,23 @@ a.btn { .categories .feeds .item.active { background: #0062BE; } -.categories .feeds .item.active .feed { - color: #fff; -} -.categories .feeds .item.empty .feed { - color: #e67e22; -} .categories .feeds .item.empty.active { background: #e67e22; } -.categories .feeds .item.empty.active .feed { - color: #fff; +.categories .feeds .item.error.active { + background: #BD362F; +} +.categories .feeds .item.empty .feed { + color: #e67e22; } .categories .feeds .item.error .feed { color: #BD362F; } +.categories .feeds .item.active .feed, +.categories .feeds .item.empty.active .feed, +.categories .feeds .item.error.active .feed { + color: #fff; +} .categories .feeds .item .feed { margin: 0; width: 165px; @@ -598,7 +600,7 @@ a.btn { } .prompt form { margin: 10px auto 20px auto; - width: 180px; + width: 200px; } .prompt input { margin: 5px auto; diff --git a/p/themes/Screwdriver/metadata.json b/p/themes/Screwdriver/metadata.json index e04c6e806..f45f1a98a 100644 --- a/p/themes/Screwdriver/metadata.json +++ b/p/themes/Screwdriver/metadata.json @@ -2,6 +2,6 @@ "name": "Screwdriver", "author": "Mister aiR", "description": "C'est un cocktail ! C'est chaud mais « fresh » à la fois. Ce thème tue du chaton.", - "version": 1.0, + "version": 1.1, "files": ["template.css","screwdriver.css"] } diff --git a/p/themes/Screwdriver/screwdriver.css b/p/themes/Screwdriver/screwdriver.css index 683eece88..665f89c71 100644 --- a/p/themes/Screwdriver/screwdriver.css +++ b/p/themes/Screwdriver/screwdriver.css @@ -206,6 +206,10 @@ a.btn { background: linear-gradient(180deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; background: -webkit-linear-gradient(top, #EDE7DE 0%, #FFF 100%); } +#loginButton.btn{ + border:none; + box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08); +} .nav_menu .btn.active, .nav_menu .btn:active, .nav_menu .dropdown-target:target ~ .btn.dropdown-toggle{ box-shadow: 0px 2px #E2972A; border-radius: 0; @@ -333,7 +337,7 @@ a.btn { .nav-head { margin: 0; background: linear-gradient(0deg, #EDE7DE 0%, #FFF 100%) #EDE7DE; - background: -webkit-linear-gradient(0deg, #EDE7DE 0%, #FFF 100%); + background: -webkit-linear-gradient(bottom, #EDE7DE 0%, #FFF 100%); text-align: right; } .nav-head .item { @@ -674,6 +678,15 @@ ul.feeds.active{ .prompt p { margin: 20px 0; } +.prompt input#username,.prompt input#passwordPlain{ + border:none; + box-shadow: 0px 1px rgba(255, 255, 255, 0.08) inset,0 -1px #171717,0px 1px rgba(255, 255, 255, 0.08); + background:#EDE7DE; +} +.prompt input#username:focus,.prompt input#passwordPlain:focus{ + border: solid 1px #E7AB34; + box-shadow: 0 0 3px #E7AB34; +} /*=== New article notification */ #new-article { @@ -755,13 +768,13 @@ ul.feeds.active{ } .flux .item.title { -text-decoration: line-through; +opacity: 0.35; } .flux.favorite .item.title { -text-decoration: none; +opacity: 1; } .flux.not_read .item.title { -text-decoration: none; +opacity: 1; } .flux.current .item.title a { color: #0f0f0f; @@ -1084,7 +1097,7 @@ text-decoration: none; text-align: center; background: #171717; box-shadow: 0 1px rgba(255,255,255,0.08); - border-radius: 0 0 0 5px; + border-radius: 0 8px 0 8px; } .aside .btn-important { display: inline-block; diff --git a/p/themes/Screwdriver/template.css b/p/themes/Screwdriver/template.css index bf421e322..ddb3f376f 100644 --- a/p/themes/Screwdriver/template.css +++ b/p/themes/Screwdriver/template.css @@ -309,6 +309,9 @@ a.btn { list-style: none; margin: 0; } +.state_unread li:not(.active)[data-unread="0"] { + display: none; +} .category { display: block; overflow: hidden; diff --git a/p/themes/base-theme/base.css b/p/themes/base-theme/base.css index c45b1812e..76ac37933 100644 --- a/p/themes/base-theme/base.css +++ b/p/themes/base-theme/base.css @@ -390,16 +390,18 @@ a.btn { /*=== Aside main page (feeds) */ .categories .feeds .item.active { } -.categories .feeds .item.active .feed { -} -.categories .feeds .item.empty .feed { -} .categories .feeds .item.empty.active { } -.categories .feeds .item.empty.active .feed { +.categories .feeds .item.error.active { +} +.categories .feeds .item.empty .feed { } .categories .feeds .item.error .feed { } +.categories .feeds .item.active .feed, +.categories .feeds .item.empty.active .feed, +.categories .feeds .item.error.active .feed { +} .categories .feeds .item .feed { margin: 0; width: 165px; |
