diff --git a/CHANGELOG b/CHANGELOG index d402c7c39..df43fe1d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 +* Version actuelle : 0.8-dev +* Date de publication 2014-0x-xx +* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) + +![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) + +# 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 + +![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png) + +# Installation +1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip) +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) diff --git a/README.md b/README.md index 8963e040c..0a22df8f1 100644 --- a/README.md +++ b/README.md @@ -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 -* Version actuelle : 0.8-dev -* Date de publication 2014-0x-xx +* Official website: http://freshrss.org +* Demo: http://demo.freshrss.org/ +* Developer: Marien Fressinaud +* Current version: 0.8-dev +* Publication date: 2014-0x-xx * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) -![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) +![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png) -# Note sur les branches -**Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond : +# 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) +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) -# 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 +# 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 -![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png) +![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png) # Installation -1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip) -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. +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. -# 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. +# 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. -# 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 : +# 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); - - Minz_Request::forward(array( - 'c' => 'index', - 'a' => 'index' - ), 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']); } - // What are you doing? you have to call this controller - // with a POST request! - Minz_Request::forward(array( - 'c' => 'importExport', - 'a' => 'index' - )); + // 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); + } + + // 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()); + + $export_files = array(); + if ($export_opml) { + $export_files['feeds.opml'] = $this->generateOpml(); + } + + if ($export_starred) { + $export_files['starred.json'] = $this->generateArticles('starred'); + } + + 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'); - } - - 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 - ); - } - } - - $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"; - } - - $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 @@ -151,6 +151,68 @@ SQL; return $this->convertToSerie($repartition); } + /** + * 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 = <<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 * 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 @@ + '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 %s', + 'article_published_on_author' => 'Dieser Artikel erschien im Original bei %s 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.
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 %s abonniert', + 'feed_added' => 'Der RSS Feed %s wurde hinzugefügt', + 'feed_not_added' => '%s 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 %s ist ungültig', + 'feed_actualized' => '%s 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 %s 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' => '+ shift 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' => 'Sie können Feeds hinzufügen.', + + 'current_user' => 'Aktuelle Nutzung', + 'default_user' => 'Nutzername des Standardnutzers (maximal 16 Zeichen - alphanumerisch)', + 'password_form' => 'Passwort
(für die Anmeldemethode per Webformular)', + 'persona_connection_email' => 'Login E-Mail Adresse
(für Mozilla Persona)', + '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.
%s?output=rss&token=%s', + '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 %s', + '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' => 'auf Github oder per Mail', + 'license' => 'Lizenz', + 'agpl3' => 'AGPL 3', + 'freshrss_description' => 'FreshRSS ist ein RSS Feedsaggregator zum selbst hosten wie zum Beispiel Kriss Feed oder Leed. Es ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstark und konfigurierbares Werkzeug.', + 'credits' => 'Credits', + 'credits_content' => 'Einige Designelemente sind von Bootstrap obwohl FreshRSS dieses Framework nicht nutzt. Icons sind vom GNOME Projekt. Open Sans Font police wurde von Steve Matteson erstellt. Favicons wurden mit getFavicon API gesammelt. FreshRSS basiert auf Minz, 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 (1 month)', '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
(OPML, Json or Zip)', + 'file_to_import_no_zip' => 'File to import
(OPML or Json)', 'import' => 'Import', + 'file_cannot_be_uploaded' => 'File cannot be uploaded!', + 'zip_error' => 'An error occured during Zip import.', + 'no_zip_extension' => 'Zip extension is not present on your server.', 'export' => 'Export', 'export_opml' => 'Export list of feeds (OPML)', 'export_starred' => 'Export your favourites', @@ -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é (1 mois)', '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
(OPML, Json ou Zip)', + 'file_to_import_no_zip' => 'Fichier à importer
(OPML ou Json)', 'import' => 'Importer', + 'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé!', + 'zip_error' => 'Une erreur est survenue durant l’import du fichier Zip.', + 'no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur.', 'export' => 'Exporter', 'export_opml' => 'Exporter la liste des flux (OPML)', 'export_starred' => 'Exporter les favoris', @@ -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; + } } ?>
  • >
        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 @@ conf->queries as $query) { ?> -
      • +
      • @@ -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 @@ nbFeed () > 0) { ?> + + + 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 @@ -61,6 +61,16 @@ +
        +
        + +
        +
        +
        +
        + +
        + + +
        +
        + +
        + +
        + +
        +
        +
        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(); + } + } ?>
      • ✇ name(); ?>
      • title (); ?>
      • date (); ?> 
      • 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 @@
        - +
        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 @@
        -

        - +
        - +
        - +
        - + +
        +
        +
        +

        - - + +

        -

        +

        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 = [feeds as $feed) { - echo "'", Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'), "',\n"; - } - ?>], +var feeds = [feeds as $feed) { ?>{url: " 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'); ?>",title: "name(); ?>"},], feed_processed = 0, feed_count = feeds.length; function initProgressBar(init) { if (init) { $("body").after("\
        \ - 0 / " + feed_count + "
        \ - \ +
        /
        \ + 0 / " + feed_count + "\
        "); } 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 @@
        - + +

        + + + feed) {?> -

        - - - feed->name(); ?> - -

        - -

        + + + - +

        - +

        @@ -41,11 +56,22 @@ function initStats() { return; } // Entry per hour + var avg_h = []; + for (var i = -1; i <= 24; i++) { + avg_h.push([i, averageHour?>]); + } Flotr.draw(document.getElementById('statsEntryPerHour'), - [repartitionHour ?>], + [{ + data: repartitionHour ?>, + bars: {horizontal: false, show: true} + }, { + data: avg_h, + lines: {show: true}, + label: 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, averageDayOfWeek?>]); + } Flotr.draw(document.getElementById('statsEntryPerDayOfWeek'), - [repartitionDayOfWeek ?>], + [{ + data: repartitionDayOfWeek ?>, + bars: {horizontal: false, show: true} + }, { + data: avg_dow, + lines: {show: true}, + label: 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, averageMonth?>]); + } Flotr.draw(document.getElementById('statsEntryPerMonth'), - [repartitionMonth ?>], + [{ + data: repartitionMonth ?>, + bars: {horizontal: false, show: true} + }, { + data: avg_m, + lines: {show: true}, + label: 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(); 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 @@ + + + + + +Redirection + + + + +

        Redirection

        + + 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.active { + background: #e67e22; +} +.categories .feeds .item.error.active { + background: #BD362F; } .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 .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.active { +} +.categories .feeds .item.error.active { } .categories .feeds .item.empty .feed { } -.categories .feeds .item.empty.active { -} -.categories .feeds .item.empty.active .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;