Merge remote-tracking branch 'FreshRSS/master'

This commit is contained in:
Clément
2017-02-15 14:14:03 +01:00
415 changed files with 36186 additions and 13880 deletions

240
CHANGELOG
View File

@@ -1,240 +0,0 @@
# Journal des modifications
## 2014-02-19 FreshRSS 0.7.1
* Mise à jour des flux plus rapide grâce à une meilleure utilisation du cache
* Utilisation dune signature MD5 du contenu intéressant pour les flux nimplémentant pas les requêtes conditionnelles
* Modification des raccourcis
* "s" partage directement si un seul moyen de partage
* Moyens de partage accessibles par "1", "2", "3", etc.
* Premier article : Home ; Dernier article : End
* Ajout du déplacement au sein des catégories / flux (via modificateurs shift et alt)
* UI
* Séparation des descriptions des raccourcis par groupes
* Revue rapide de la page de connexion
* Amélioration de l'affichage des notifications sur mobile
* Revue du système de rafraîchissement des flux
* Meilleure gestion de la file de flux à rafraîchir en JSON
* Rafraîchissement uniquement pour les flux non rafraîchis récemment
* Possibilité donnée aux anonymes de rafraîchir les flux
* SimplePie
* Mise à jour de la lib
* Corrige fuite de mémoire
* Meilleure tolérance aux flux invalides
* Corrections divers
* Ne déplie plus l'article lors du clic sur l'icône lien externe
* Ne boucle plus à la fin de la navigation dans les articles
* Suppression du champ category.color inutile
* Corrige bug redirection infinie (Persona)
* Amélioration vérification de la requête POST
* Ajout d'un verrou lorsqu'une action mark_read ou mark_favorite est en cours
## 2014-01-29 FreshRSS 0.7
* Nouveau mode multi-utilisateur
* Lutilisateur par défaut (administrateur) peut créer et supprimer dautres utilisateurs
* Nécessite un contrôle daccès, soit :
* par le nouveau mode de connexion par formulaire (nom dutilisateur + mot de passe)
* relativement sûr même sans HTTPS (le mot de passe nest pas transmis en clair)
* requiert JavaScript et PHP 5.3+
* par HTTP (par exemple sous Apache en créant un fichier ./p/i/.htaccess et .htpasswd)
* le nom dutilisateur HTTP doit correspondre au nom dutilisateur FreshRSS
* par Mozilla Persona, en renseignant ladresse courriel des utilisateurs
* Installateur supportant les mises à jour :
* Depuis une v0.6, placer application.ini et Configuration.array.php dans le nouveau répertoire “./data/”
(voir réorganisation ci-dessous)
* Pour les versions suivantes, juste garder le répertoire “./data/”
* Rafraîchissement automatique du nombre darticles non lus toutes les deux minutes (utilise le cache HTTP à bon escient)
* Permet aussi de conserver la session valide, surtout dans le cas de Persona
* Nouvelle page de statistiques (nombres darticles par jour / catégorie)
* Importation OPML instantanée et plus tolérante
* Nouvelle gestion des favicons avec téléchargement en parallèle
* Nouvelles options
* Réorganisation des options
* Gestion des utilisateurs
* Améliorations partage vers Shaarli, Poche, Diaspora*, Facebook, Twitter, Google+, courriel
* Raccourci s par défaut
* Permet la suppression de tous les articles dun flux
* Option pour marquer les articles comme lus dès la réception
* Permet de configurer plus finement le nombre darticles minimum à conserver par flux
* Permet de modifier la description et ladresse dun flux RSS ainsi que le site Web associé
* Nouveau raccourci pour ouvrir/fermer un article (c par défaut)
* Boutons pour effacer les logs et pour purger les vieux articles
* Nouveaux filtres daffichage : seulement les articles favoris, et seulement les articles lus
* SQL :
* Nouveau moteur de recherche, aussi accessible depuis la vue mobile
* Mots clefs de recherche “intitle:”, “inurl:”, “author:”
* Les articles sont triés selon la date de leur ajout dans FreshRSS plutôt que la date déclarée (souvent erronée)
* Permet de marquer tout comme lu sans affecter les nouveaux articles arrivés en cours de lecture
* Permet une pagination efficace
* Refactorisation
* Les tables sont préfixées avec le nom dutilisateur afin de permettre le mode multi-utilisateurs
* Amélioration des performances
* Tolère un beaucoup plus grand nombre darticles
* Compression des données côté MySQL plutôt que côté PHP
* Incompatible avec la version 0.6 (nécessite une mise à jour grâce à linstallateur)
* Affichage de la taille de la base de données dans FreshRSS
* Correction problème de marquage de tous les favoris comme lus
* HTML5 :
* Support des balises HTML5 audio, video, et éléments associés
* Utilisation de preload="none", et réécriture correcte des adresses, aussi en HTTPS
* Protection HTML5 des iframe (sandbox="allow-scripts allow-same-origin")
* Filtrage des object et embed
* Chargement différé HTML5 (postpone="") pour iframe et video
* Chargement différé JavaScript pour iframe
* CSS :
* Nouveau thème sombre
* Chargement plus robuste des thèmes
* Meilleur support des longs titres darticles sur des écrans étroits
* Meilleure accessibilité
* FreshRSS fonctionne aussi en mode dégradé sans images (alternatives Unicode) et/ou sans CSS
* Diverses améliorations
* PHP :
* Encore plus tolérant pour les flux comportant des erreurs
* Mise à jour automatique de lURL du flux (en base de données) lorsque SimplePie découvre quelle a changé
* Meilleure gestion des caractères spéciaux dans différents cas
* Compatibilité PHP 5.5+ avec OPcache
* Amélioration des performances
* Chargement automatique des classes
* Alternative dans le cas dabsence de librairie JSON
* Pour le développement, le cache HTTP peut être désactivé en créant un fichier “./data/no-cache.txt”
* Réorganisation des fichiers et répertoires, en particulier :
* Tous les fichiers utilisateur sont dans “./data/” (y compris “cache”, “favicons”, et “log”)
* Déplacement de “./app/configuration/application.ini” vers “./data/config.php”
* Meilleure sécurité et compatibilité
* Déplacement de “./public/data/Configuration.array.php” vers “./data/*_user.php”
* Déplacement de “./public/” vers “./p/”
* Déplacement de “./public/index.php” vers “./p/i/index.php” (voir cookie ci-dessous)
* Déplacement de “./actualize_script.php” vers “./app/actualize_script.php” (pour une meilleure sécurité)
* Pensez à mettre à jour votre Cron !
* Divers :
* Nouvelle politique de cookie de session (témoin de connexion)
* Utilise un nom poli “FreshRSS” (évite des problèmes avec certains filtres)
* Se limite au répertoire “./FreshRSS/p/i/” pour de meilleures performances HTTP
* Les images, CSS, scripts sont servis sans cookie
* Utilise “HttpOnly” pour plus de sécurité
* Nouvel “agent utilisateur” exposé lors du téléchargement des flux, par exemple :
* “FreshRSS/0.7 (Linux; http://freshrss.org) SimplePie/1.3.1”
* Script dactualisation avec plus de messages
* Sur la sortie standard, ainsi que dans le log système (syslog)
* Affichage du numéro de version dans "À propos"
## 2013-11-21 FreshRSS 0.6.1
* Corrige bug chargement du JavaScript
* Affiche un message derreur plus explicite si fichier de configuration inaccessible
## 2013-11-17 FreshRSS 0.6
* Nettoyage du code JavaScript + optimisations
* Utilisation dadresses relatives
* Amélioration des performances coté client
* Mise à jour automatique du nombre darticles non lus
* Corrections traductions
* Mise en cache de FreshRSS
* Amélioration des retours utilisateur lorsque la configuration nest pas bonne
* Actualisation des flux après une importation OPML
* Meilleure prise en charge des flux RSS invalides
* Amélioration de la vue globale
* Possibilité de personnaliser les icônes de lecture
* Suppression de champs lors de linstallation (base_url et sel)
* Correction bugs divers
## 2013-10-15 FreshRSS 0.5.1
* Correction bug des catégories disparues
* Correction traduction i18n/fr et i18n/en
* Suppression de certains appels à la feuille de style fallback.css
## 2013-10-12 FreshRSS 0.5.0
* Possibilité dinterdire la lecture anonyme
* Option pour garder lhistorique dun flux
* Lors dun clic sur “Marquer tous les articles comme lus”, FreshRSS peut désormais sauter à la prochaine catégorie / prochain flux avec des articles non lus.
* Ajout dun token pour accéder aux flux RSS générés par FreshRSS sans nécessiter de connexion
* Possibilité de partager vers Facebook, Twitter et Google+
* Possibilité de changer de thème
* Le menu de navigation (article précédent / suivant / haut de page) a été ajouté à la vue non mobile
* La police OpenSans est désormais appliquée
* Amélioration de la page de configuration
* Une meilleure sortie pour limprimante
* Quelques retouches du design par défaut
* Les vidéos ne dépassent plus du cadre de lécran
* Nouveau logo
* Possibilité dajouter un préfixe aux tables lors de linstallation
* Ajout dun champ en base de données keep_history à la table feed
* Si possible, création automatique de la base de données si elle nexiste pas lors de linstallation
* Lutilisation dUTF-8 est forcée
* Le marquage automatique au défilement de la page a été amélioré
* La vue globale a été énormément améliorée et est beaucoup plus utile
* Amélioration des requêtes SQL
* Amélioration du JavaScript
* Correction bugs divers
## 2013-07-02 FreshRSS 0.4.0
* Correction bug et ajout notification lors de la phase dinstallation
* Affichage derreur si fichier OPML invalide
* Les tags sont maintenant cliquables pour filtrer dessus
* Amélioration vue mobile (boutons plus gros et ajout dune barre de navigation)
* Possibilité dajouter directement un flux dans une catégorie dès son ajout
* Affichage des flux en erreur (injoignable par exemple) en rouge pour les différencier
* Possibilité de changer les noms des flux
* Ajout dune option (désactivable donc) pour charger les images en lazyload permettant de ne pas charger toutes les images dun coup
* Le framework Minz est maintenant directement inclus dans larchive (plus besoin de passer par ./build.sh)
* Amélioration des performances pour la récupération des flux tronqués
* Possibilité dimporter des flux sans catégorie lors de limport OPML
* Suppression de “lAPI” (qui était de toute façon très basique) et de la fonctionnalité de “notes”
* Amélioration de la recherche (garde en mémoire si lon a sélectionné une catégorie) par exemple
* Modification apparence des balises hr et pre
* Meilleure vérification des champs de formulaire
* Remise en place du mode “endless” (permettant de simplement charger les articles qui suivent plutôt que de charger une nouvelle page)
* Ajout dune page de visualisation des logs
* Ajout dune option pour optimiser la BDD (diminue sa taille)
* Ajout des vues lecture et globale (assez basique)
* Les vidéos YouTube ne débordent plus du cadre sur les petits écrans
* Ajout dune option pour marquer les articles comme lus lors du défilement (et suppression de celle au chargement de la page)
## 2013-05-05 FreshRSS 0.3.0
* Fallback pour les icônes SVG (utilisation de PNG à la place)
* Fallback pour les propriétés CSS3 (utilisation de préfixes)
* Affichage des tags associés aux articles
* Internationalisation de lapplication (gestion des langues anglaise et française)
* Gestion des flux protégés par authentification HTTP
* Mise en cache des favicons
* Création dun logo *temporaire*
* Affichage des vidéos dans les articles
* Gestion de la recherche et filtre par tags pleinement fonctionnels
* Création dun vrai script CRON permettant de mettre tous les flux à jour
* Correction bugs divers
## 2013-04-17 FreshRSS 0.2.0
* Création dun installateur
* Actualisation des flux en Ajax
* Partage par mail et Shaarli ajouté
* Export par flux RSS
* Possibilité de vider une catégorie
* Possibilité de sélectionner les catégories en vue mobile
* Les flux peuvent être sortis du flux principal (système de priorité)
* Amélioration ajout / import / export des flux
* Amélioration actualisation (meilleure gestion des erreurs)
* Améliorations CSS
* Changements dans la base de données
* Màj de la librairie SimplePie
* Flux sans auteurs gérés normalement
* Correction bugs divers
## 2013-04-08 FreshRSS 0.1.0
* “Première” version

668
CHANGELOG.md Normal file
View File

@@ -0,0 +1,668 @@
# Changelog
## 2016-12-26 FreshRSS 1.6.2
* Features
* Add git compatibility in Web update system [#1357](https://github.com/FreshRSS/FreshRSS/issues/1357)
* Requires that the initial installation is done with git
* New option `limits.cookie_duration` in `data/config.php` to set the login cookie duration [#1384](https://github.com/FreshRSS/FreshRSS/issues/1384)
* SQL
* More robust export function in the case of large datasets [#1372](https://github.com/FreshRSS/FreshRSS/issues/1372)
* CLI
* New command `./cli/user-info.php` to get some user information [#1345](https://github.com/FreshRSS/FreshRSS/issues/1345)
* Bug fixing
* Fix bug in estimating last user activity [#1358](https://github.com/FreshRSS/FreshRSS/issues/1358)
* PostgreSQL: fix bug when updating cached values [#1360](https://github.com/FreshRSS/FreshRSS/issues/1360)
* Fix bug in confirmation before marking as read [#1348](https://github.com/FreshRSS/FreshRSS/issues/1348)
* Fix small bugs in installer [#1363](https://github.com/FreshRSS/FreshRSS/pull/1363)
* Allow slash in database hostname, when using sockets [#1364](https://github.com/FreshRSS/FreshRSS/issues/1364)
* Add curl user-agent to retrieve favicons [#1380](https://github.com/FreshRSS/FreshRSS/issues/1380)
* Send login cookie only once [#1398](https://github.com/FreshRSS/FreshRSS/pull/1398)
* Add a check for PHP extension fileinfo [#1375](https://github.com/FreshRSS/FreshRSS/issues/1375)
## 2016-11-02 FreshRSS 1.6.1
* Bug fixing
* Fix regression introduced in 1.6.0 when refreshing articles with *Mark updated articles as unread* [#1349](https://github.com/FreshRSS/FreshRSS/issues/1349)
## 2016-10-30 FreshRSS 1.6.0
* CLI
* New Command-Line Interface (CLI) [#1095](https://github.com/FreshRSS/FreshRSS/issues/1095)
* Install, add/delete users, actualize, import/export. See [CLI documentation](./cli/README.md).
* API
* Support for editing feeds and categories from client applications [#1254](https://github.com/FreshRSS/FreshRSS/issues/1254)
* Compatibility:
* Support for PostgreSQL [#416](https://github.com/FreshRSS/FreshRSS/issues/416)
* New client supporting FreshRSS on Linux: FeedReader 2.0+ [#1252](https://github.com/FreshRSS/FreshRSS/issues/1252)
* Features
* Rework the “mark as read during scroll” option, enabled by default for new users [#1258](https://github.com/FreshRSS/FreshRSS/issues/1258), [#1309](https://github.com/FreshRSS/FreshRSS/pull/1309)
* Including a *keep unread* function [#1327](https://github.com/FreshRSS/FreshRSS/pull/1327)
* In a multi-user context, take better advantage of other users refreshes [#1280](https://github.com/FreshRSS/FreshRSS/pull/1280)
* Better control of number of entries per page or RSS feed [#1249](https://github.com/FreshRSS/FreshRSS/issues/1249)
* Since X hours: `https://freshrss.example/i/?a=rss&hours=3`
* Explicit number: `https://freshrss.example/i/?a=rss&nb=10`
* Limited by `min_posts_per_rss` and `max_posts_per_rss` in user config
* Support custom ports `localhost:3306` for database servers [#1241](https://github.com/FreshRSS/FreshRSS/issues/1241)
* Add date to exported files [#1240](https://github.com/FreshRSS/FreshRSS/issues/1240)
* Auto-refresh favicons once or twice a month [#1181](https://github.com/FreshRSS/FreshRSS/issues/1181), [#1298](https://github.com/FreshRSS/FreshRSS/issues/1298)
* Cron updates will also refresh favicons every 2 weeks [#1306](https://github.com/FreshRSS/FreshRSS/pull/1306)
* Bug fixing
* Correction of bugs related to CSRF tokens introduced in version 1.5.0 [#1253](https://github.com/FreshRSS/FreshRSS/issues/1253), [44f22ab](https://github.com/FreshRSS/FreshRSS/pull/1261/commits/d9bf9b2c6f0b2cc9dec3b638841b7e3040dcf46f)
* Fix bug in Global view introduced in version 1.5.0 [#1269](https://github.com/FreshRSS/FreshRSS/pull/1269)
* Fix sharing bug [#1289](https://github.com/FreshRSS/FreshRSS/issues/1289)
* Fix bug in auto-loading more articles after marking an article as un-read [#1318](https://github.com/FreshRSS/FreshRSS/issues/1318)
* Fix bug during import of favourites [#1315](https://github.com/FreshRSS/FreshRSS/pull/1315), [#1312](https://github.com/FreshRSS/FreshRSS/issues/1312)
* Fix bug not respecting language option for new users [#1273](https://github.com/FreshRSS/FreshRSS/issues/1273)
* Bug in example of URL for FreshRSS RSS output with token [#1274](https://github.com/FreshRSS/FreshRSS/issues/1274)
* Security
* Prevent `<a target="_blank">` attacks with `window.opener` [#1245](https://github.com/FreshRSS/FreshRSS/issues/1245)
* Updated gitignore rules to keep user directories during a `git clean -f -d` [#1307](https://github.com/FreshRSS/FreshRSS/pull/1307)
* Extensions
* Allow extensions for default account in anonymous mode [#1288](https://github.com/FreshRSS/FreshRSS/pull/1288)
* Trigger a `freshrss:load-more` JavaScript event to help extensions [#1278](https://github.com/FreshRSS/FreshRSS/issues/1278)
* SQL
* Slightly modified several SQL requests (MySQL, SQLite) to simplify support of PostgreSQL [#1195](https://github.com/FreshRSS/FreshRSS/pull/1195)
* Increase performances by removing a superfluous category request [#1316](https://github.com/FreshRSS/FreshRSS/pull/1316)
* I18n
* Fix some messages during installation [#1339](https://github.com/FreshRSS/FreshRSS/pull/1339)
* UI
* Fix CSS line-height bug with `<sup>` in dates (English, Russian, Turkish) [#1340](https://github.com/FreshRSS/FreshRSS/pull/1340)
* Disable *Mark all as read* before confirmation script is loaded [#1342](https://github.com/FreshRSS/FreshRSS/issues/1342)
* Download icon 💾 for podcasts [#1236](https://github.com/FreshRSS/FreshRSS/issues/1236)
* SimplePie
* Fix auto-discovery of RSS feeds in Web pages served as `text/xml` [#1264](https://github.com/FreshRSS/FreshRSS/issues/1264)
* Misc.
* Removed *resource-priorities* attributes (`defer`, `lazyload`), deprecated by W3C [#1222](https://github.com/FreshRSS/FreshRSS/pull/1222)
## 2016-08-29 FreshRSS 1.5.0
* Compatibility
* Require at least MySQL 5.5.3+ [#1153](https://github.com/FreshRSS/FreshRSS/issues/1153)
* Require at least PHP 5.3.3+ [#1183](https://github.com/FreshRSS/FreshRSS/pull/1183)
* Restore compatibility with PHP 5.3.3 [#1208](https://github.com/FreshRSS/FreshRSS/issues/1208)
* Restore compatibility with Microsoft Internet Explorer 11 / Edge [#772](https://github.com/FreshRSS/FreshRSS/issues/772)
* Features
* Mark a search as read [#608](https://github.com/FreshRSS/FreshRSS/issues/608)
* Support for full Unicode such as emoji 💕 in MySQL with utf8mb4 [#1153](https://github.com/FreshRSS/FreshRSS/issues/1153)
* FreshRSS will automatically migrate MySQL tables to utf8mb4 the first time it is needed.
* Security
* Remove Mozilla Persona login (the service closes on 2016-11-30) [#1052](https://github.com/FreshRSS/FreshRSS/issues/1052)
* Use Referrer Policy `<meta name="referrer" content="never" />` for anonymizing HTTP Referer [#955](https://github.com/FreshRSS/FreshRSS/issues/955)
* Implement CSRF tokens for POST security [#570](https://github.com/FreshRSS/FreshRSS/issues/570)
* Bug fixing
* Fixed scroll in log view [#1178](https://github.com/FreshRSS/FreshRSS/issues/1178)
* Fixed JavaScript bug when articles were not always marked as read [#1123](https://github.com/FreshRSS/FreshRSS/issues/1123)
* Fixed Apache Etag issue that prevented caching [#1199](https://github.com/FreshRSS/FreshRSS/pull/1199)
* Fixed OPML import of categories [#1202](https://github.com/FreshRSS/FreshRSS/issues/1202)
* Fixed PubSubHubbub callback address bug on some configurations [1229](https://github.com/FreshRSS/FreshRSS/pull/1229)
* UI
* Use sticky category column [#1172](https://github.com/FreshRSS/FreshRSS/pull/1172)
* Updated to jQuery 3.1.0 and several JavaScript fixes (e.g. drag & drop) [#1197](https://github.com/FreshRSS/FreshRSS/pull/1197)
* API
* Add API link in FreshRSS profile settings to ease set-up [#1186](https://github.com/FreshRSS/FreshRSS/pull/1186)
* Misc.
* Work-around for SuperFeeder time-outs during PubSubHubbub registration [#1184](https://github.com/FreshRSS/FreshRSS/pull/1184)
* JSHint of JavaScript code and better initialisation [#1196](https://github.com/FreshRSS/FreshRSS/pull/1196)
* Updated credits, and images in README [#1201](https://github.com/FreshRSS/FreshRSS/issues/1201)
## 2016-07-23 FreshRSS 1.4.0
## 2016-06-12 FreshRSS 1.3.2-beta
* Compatibility
* Require at least PHP 5.3+ (drop PHP 5.2) [#1133](https://github.com/FreshRSS/FreshRSS/pull/1133)
* Features
* Support for MySQL 5.7+ (e.g. Ubuntu 16.04 LTS) [#1132](https://github.com/FreshRSS/FreshRSS/pull/1132)
* Speed optimization for HTTP/2 [#1133](https://github.com/FreshRSS/FreshRSS/pull/1133)
* API support for REDIRECT_* HTTP headers (fcgi) [#1128](https://github.com/FreshRSS/FreshRSS/issues/1128)
* SimplePie
* Support for feeds with invalid whitespace [#1142](https://github.com/FreshRSS/FreshRSS/issues/1142)
* Bug fixing
* Fix bug when adding feeds with passwords [#1137](https://github.com/FreshRSS/FreshRSS/pull/1137)
* Fix validator link [#1147](https://github.com/FreshRSS/FreshRSS/pull/1147)
* Fix Favicon small bugs [#1135](https://github.com/FreshRSS/FreshRSS/pull/1135)
* Security
* CSP compatibility for homepage [#1120](https://github.com/FreshRSS/FreshRSS/pull/1120)
* I18n
* Draft of Russian [#1085](https://github.com/FreshRSS/FreshRSS/pull/1085)
* Misc.
* Change default feed timeout to 15 seconds [#1146](https://github.com/FreshRSS/FreshRSS/pull/1146)
* Updated Wallabag v2 [#1150](https://github.com/FreshRSS/FreshRSS/pull/1150)
## 2016-03-11 FreshRSS 1.3.1-beta
* Security
* Added CSP `Content-Security-Policy: default-src 'self'; child-src *; frame-src *; img-src * data:; media-src *` [#1075](https://github.com/FreshRSS/FreshRSS/issues/1075), [#1114](https://github.com/FreshRSS/FreshRSS/issues/1114)
* Added `X-Content-Type-Options: nosniff` [#1116](https://github.com/FreshRSS/FreshRSS/pull/1116)
* Cookie with `Secure` tag when used over HTTPS [#1117](https://github.com/FreshRSS/FreshRSS/pull/1117)
* Limit API post input to 1MB [#1118](https://github.com/FreshRSS/FreshRSS/pull/1118)
* Features
* New list of domains for which to force HTTPS (for images, videos, iframes…) defined in `./data/force-https.default.txt` and `./data/force-https.txt` [#1083](https://github.com/FreshRSS/FreshRSS/issues/1083)
* In particular useful for privacy and to avoid mixed content errors, e.g. to see YouTube videos when FreshRSS is in HTTPS
* Add sharing with “Journal du Hacker” [#1056](https://github.com/FreshRSS/FreshRSS/pull/1056)
* UI
* Updated to jQuery 2.2.1 and changed code for auto-load on scroll [#1050](https://github.com/FreshRSS/FreshRSS/pull/1050), [#1091](https://github.com/FreshRSS/FreshRSS/pull/1091)
* I18n
* Turkish [#1073](https://github.com/FreshRSS/FreshRSS/issues/1073)
* Bug fixing
* Fixed OPML import title bug [#1048](https://github.com/FreshRSS/FreshRSS/issues/1048)
* Fixed upgrade bug with SQLite when articles were marked as unread [#1049](https://github.com/FreshRSS/FreshRSS/issues/1049)
* Fixed error when deleting feeds from statistics page [#1047](https://github.com/FreshRSS/FreshRSS/issues/1047)
* Fixed several small bugs in global and reader view [#1050](https://github.com/FreshRSS/FreshRSS/pull/1050)
* Fixed sharing bug with PHP7 [#1072](https://github.com/FreshRSS/FreshRSS/issues/1072)
* Fixed fall-back when php-json is not installed [#1092](https://github.com/FreshRSS/FreshRSS/issues/1092)
* API
* Possibility to show only read items [#1035](https://github.com/FreshRSS/FreshRSS/pull/1035)
* Misc.
* Filters `<img />` attributes `srcset` and `sizes` [#1077](https://github.com/FreshRSS/FreshRSS/issues/1077), [#1086](https://github.com/FreshRSS/FreshRSS/pull/1086)
* Implement PubSubHubbub unsubscribe responses [#1058](https://github.com/FreshRSS/FreshRSS/issues/1058)
* Restored some compatibility with PHP 5.2 [#1055](https://github.com/FreshRSS/FreshRSS/issues/1055)
* Check for extension php-xml during install [#1094](https://github.com/FreshRSS/FreshRSS/issues/1094)
* Updated the sharing with Movim [#1030](https://github.com/FreshRSS/FreshRSS/pull/1030)
## 2015-11-03 FreshRSS 1.2.0 / 1.3.0-beta
* Features
* Share with Movim [#992](https://github.com/FreshRSS/FreshRSS/issues/992)
* New option to allow robots / search engines [#938](https://github.com/FreshRSS/FreshRSS/issues/938)
* Security
* Invalid logins now return HTTP 403, to be easier to catch (e.g. fail2ban) [#1015](https://github.com/FreshRSS/FreshRSS/issues/1015)
* UI
* Remove "title" field during installation [#858](https://github.com/FreshRSS/FreshRSS/issues/858)
* Visual alert on categories containing feeds in error [#984](https://github.com/FreshRSS/FreshRSS/pull/984)
* I18n
* Italian [#1003](https://github.com/FreshRSS/FreshRSS/issues/1003)
* Misc.
* Support reverse proxy [#975](https://github.com/FreshRSS/FreshRSS/issues/975)
* Make auto-update server URL alterable [#1019](https://github.com/FreshRSS/FreshRSS/issues/1019)
## 2015-09-12 FreshRSS 1.1.3-beta
* UI
* Configuration page for global settings such as limits [#958](https://github.com/FreshRSS/FreshRSS/pull/958)
* Add feed ID in articles to ease styling [#953](https://github.com/FreshRSS/FreshRSS/issues/953)
* I18n
* Dutch [#949](https://github.com/FreshRSS/FreshRSS/issues/949)
* Bug fixing
* Session cookie bug [#924](https://github.com/FreshRSS/FreshRSS/issues/924)
* Better error handling for PubSubHubbub [#939](https://github.com/FreshRSS/FreshRSS/issues/939)
* Fix tag search link from articles [#970](https://github.com/FreshRSS/FreshRSS/issues/970)
* Fix all queries deleted when deleting a feed or category [#982](https://github.com/FreshRSS/FreshRSS/pull/982)
## 2015-07-30 FreshRSS 1.1.2-beta
* Features
* Support for PubSubHubbub for instant notifications from compatible Web sites. [#312](https://github.com/FreshRSS/FreshRSS/issues/312)
* cURL options to use a proxy for retrieving feeds. [#897](https://github.com/FreshRSS/FreshRSS/issues/897) [#675](https://github.com/FreshRSS/FreshRSS/issues/675)
* Allow anonymous users to create an account. [#679](https://github.com/FreshRSS/FreshRSS/issues/679)
* Security
* cURL options to verify or not SSL/TLS certificates (now enabled by default). [#897](https://github.com/FreshRSS/FreshRSS/issues/897) [#502](https://github.com/FreshRSS/FreshRSS/issues/502)
* Support for SSL connection to MySQL. [#868](https://github.com/FreshRSS/FreshRSS/issues/868)
* Workaround for browsers that have disabled support for `<form autocomplete="off">`. [#880](https://github.com/FreshRSS/FreshRSS/issues/880)
* UI
* Force UTF-8 for responses. [#870](https://github.com/FreshRSS/FreshRSS/issues/870)
* Increased pagination limit to 500 articles. [#872](https://github.com/FreshRSS/FreshRSS/issues/872)
* Improved UI for installation. [#855](https://github.com/FreshRSS/FreshRSS/issues/855)
* Misc.
* PHP 7 officially supported (~70% speed improvements on early tests). [#889](https://github.com/FreshRSS/FreshRSS/issues/889)
* Restore support for PHP 5.2.1+. [#214a5cc](https://github.com/Alkarex/FreshRSS/commit/214a5cc9a4c2b821961bc21f22b4b08e34b5be68) [#894](https://github.com/FreshRSS/FreshRSS/issues/894)
* Support for data-src for images of articles retrieved via the full-content module. [#877](https://github.com/FreshRSS/FreshRSS/issues/877)
* Add a couple of default feeds for fresh installations. [#886](https://github.com/FreshRSS/FreshRSS/issues/886)
* Changed some log visibilities. [#885](https://github.com/FreshRSS/FreshRSS/issues/885)
* Fix broken links for extension script / style files. [#862](https://github.com/FreshRSS/FreshRSS/issues/862)
* Load default configuration during installation to avoid hard-coded values. [#890](https://github.com/FreshRSS/FreshRSS/issues/890)
* Fix non-consistent behaviour in Minz_Request::getBaseUrl() and introduce Minz_Request::guessBaseUrl(). [#906](https://github.com/FreshRSS/FreshRSS/issues/906)
* Generate `base_url` during the installation and add a `pubsubhubbub_enabled` configuration key. [#865](https://github.com/FreshRSS/FreshRSS/issues/865)
* Load configuration by recursion to overwrite array values. [#923](https://github.com/FreshRSS/FreshRSS/issues/923)
* Cast `$limits` configuration values in integer. [#925](https://github.com/FreshRSS/FreshRSS/issues/925)
* Don't hide errors in configuration. [#920](https://github.com/FreshRSS/FreshRSS/issues/920)
## 2015-05-31 FreshRSS 1.1.1 (beta)
* Features
* New option to detect and mark updated articles as unread.
* Support for internationalized domain name (IDN).
* Improved logic for automatic deletion of old articles.
* API
* Work-around for News+ bug when there is no unread article on the server.
* UI
* New confirmation message when leaving a configuration page without saving the changes.
* Bug fixing
* Corrected bug introduced in previous beta about handling of HTTP 301 (feeds that have changed address)
* Corrected bug in FreshRSS RSS feeds.
* Security
* Sanitize HTTP request header `Host`.
* Misc.
* Attempt to better handle encoded article titles.
## 2015-01-31 FreshRSS 1.0.0 / 1.1.0 (beta)
* UI
* Slider math with Dark theme
* Add a message if request failed for mark as read / favourite
* I18n
* Fix some sentences
* Add German as a supported language
* Add some indications on password format
* Bug fixing
* Some shortcuts was never saved
* Global view didn't work if set by default
* Minz_Error was badly raised
* Feed update failed if nothing had changed (MySQL only)
* CRON task failed with multiple users
* Tricky bug caused by cookie path
* Email sharing was badly supported (no urlencode())
* Misc.
* Add a CREDIT file with contributor names
* Update lib_opml
* Default favicon is now served by HTTP code 200
* Change calls to syslog by Minz_Log::notice
* HTTP credentials are no longer logged
## 2015-01-15 FreshRSS 0.9.4 (beta)
* Feature
* Extension system (!!): some extensions are available at https://github.com/FreshRSS/Extensions
* Refactoring
* Front controller (FreshRSS class)
* Configuration system
* Sharing system
* New data files organization
* Updates
* Remove restriction of 1h for updates
* Show the current version of FreshRSS and the next one
* UI
* Remove the "sticky position" of the feed aside (moved into an extension)
* "Show password" shows the password only while the user is pressing the mouse.
## 2014-12-12 FreshRSS 0.9.3 (beta)
* SimplePie
* Support for content-type application/x-rss+xml
* New force_feed option (for feeds sent with the wrong content-type / MIME) by adding #force_feed at the end of the feed URL
* Improved error messages
* Statistics
* Add information on feed repartition pages
* Add percent repartition for the bigger feeds
* UI
* New theme selector
* Update Screwdriver theme
* Add BlueLagoon theme by Mister aiR
* Misc.
* Add option to remove articles after reading them
* Add comments
* Refactor i18n system to avoid loading unnecessary strings
* Fix security issue in Minz_Error::error() method
* Fix redirection after refreshing a given feed
## 2014-10-31 FreshRSS 0.9.2 (beta)
* UI
* New subscription page (introduce .box items)
* Change feed category by drag and drop
* New feed aside on the main page
* New configuration / administration organization
* Configuration
* New options in config.php for cache duration, timeout, max inactivity, max number of feeds and categories per user.
* Refactoring
* Refactor authentication system (introduce FreshRSS_Auth model)
* Refactor indexController (introduce FreshRSS_Context model)
* Use ```_t()```, ```_i()```, ```_url()```, ```Minz_Request::good()``` and ```Minz_Request::bad()``` as much as possible
* Refactor javascript_vars.phtml
* Better coding style
* I18n
* Introduce a new system for i18n keys (not finished yet)
* Misc.
* Fix global view (did not work anymore)
* Add do_post_update for update system
* Introduce ```checkInstallAction``` to test if FreshRSS installation is ok
## 2014-10-09 FreshRSS 0.8.1 / 0.9.1 (beta)
* UI
* Add a space after tag icon
* Statistics
* Add an average per day on the 30-day period graph
* Add percent of total on top 10 feed
* Bug fixes
* Fix "mark as read" in global view
* Fix "read all" shortcut
* Fix categories not appearing when adding a new feed (GET action)
* Fix enclosure problem
* Fix getExtension() on PHP < 5.3.7
## 2014-09-26 FreshRSS 0.8.0 / 0.9.0 (beta)
* UI
* New interface for statistics
* Fix filter buttons
* Number of articles divided by 2 in reading view
* Redesign of bigMarkAsRead
* Features
* New automatic update system
* New reset auth system
* Security
* "Mark as read" requires POST requests for several articles
* Test HTTP REFERER in install.php
* Configuration
* New "Show all articles" / "Show only unread" / "Adjust viewing" option
* New notification timeout option
* Misc.
* Improve coding style + comments
* Fix SQLite bug "ON DELETE CASCADE"
* Improve performance when importing articles
## 2014-08-24 FreshRSS 0.7.4
* UI
* Hide categories/feeds with unread articles when showing only unread articles
* Dynamic favicon showing the number of unread articles
* New theme: Screwdriver by Mister aiR
* Statistics
* New page with article repartition
* 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
* Bug fixes in export function, add/remove users, keyboard shortcuts, etc.
## 2014-07-21 FreshRSS 0.7.3
* New options
* Add system of user queries which are shortcuts to filter the view
* New TTL option to limit the frequency at which feeds are refreshed (by cron or manual refresh button).
It is still possible to manually refresh an individual feed at a higher frequency.
* SQL
* Add support for SQLite (beta) in addition to MySQL
* SimplePie
* Complies with HTTP "301 Moved Permanently" responses by automatically updating the URL of feeds that have changed address.
* Themes
* Flat and Dark designs are based on same template file as Origine
* Statistics
* Refactor code
* Add an idle feed page
* Misc
* Several bug fixes
* Add confirmation option when marking all articles as read
* Fix some typo
## 2014-06-13 FreshRSS 0.7.2
* API compatible with Google Reader API level 2
* FreshRSS can now be used from e.g.:
* (Android) News+ https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader
* (Android) EasyRSS https://github.com/Alkarex/EasyRSS
* Basic support for audio and video podcasts
* Searching
* New search filters date: and pubdate: accepting ISO 8601 date intervals such as `date:2013-2014` or `pubdate:P1W`
* Possibility to combine search filters, e.g. `date:2014-05 intitle:FreshRSS intitle:Open great reader #Internet`
* Change nav menu with more buttons instead of dropdown menus and add some filters
* New system of import / export
* Support OPML, Json (like Google Reader) and ZIP archives
* Can export and import articles (specific option for favorites)
* Refactor "Origine" theme
* Some improvements
* Based on a template file (other themes will use it too)
## 2014-02-19 FreshRSS 0.7.1
* Mise à jour des flux plus rapide grâce à une meilleure utilisation du cache
* Utilisation dune signature MD5 du contenu intéressant pour les flux nimplémentant pas les requêtes conditionnelles
* Modification des raccourcis
* "s" partage directement si un seul moyen de partage
* Moyens de partage accessibles par "1", "2", "3", etc.
* Premier article : Home ; Dernier article : End
* Ajout du déplacement au sein des catégories / flux (via modificateurs shift et alt)
* UI
* Séparation des descriptions des raccourcis par groupes
* Revue rapide de la page de connexion
* Amélioration de l'affichage des notifications sur mobile
* Revue du système de rafraîchissement des flux
* Meilleure gestion de la file de flux à rafraîchir en JSON
* Rafraîchissement uniquement pour les flux non rafraîchis récemment
* Possibilité donnée aux anonymes de rafraîchir les flux
* SimplePie
* Mise à jour de la lib
* Corrige fuite de mémoire
* Meilleure tolérance aux flux invalides
* Corrections divers
* Ne déplie plus l'article lors du clic sur l'icône lien externe
* Ne boucle plus à la fin de la navigation dans les articles
* Suppression du champ category.color inutile
* Corrige bug redirection infinie (Persona)
* Amélioration vérification de la requête POST
* Ajout d'un verrou lorsqu'une action mark_read ou mark_favorite est en cours
## 2014-01-29 FreshRSS 0.7
* Nouveau mode multi-utilisateur
* Lutilisateur par défaut (administrateur) peut créer et supprimer dautres utilisateurs
* Nécessite un contrôle daccès, soit :
* par le nouveau mode de connexion par formulaire (nom dutilisateur + mot de passe)
* relativement sûr même sans HTTPS (le mot de passe nest pas transmis en clair)
* requiert JavaScript et PHP 5.3+
* par HTTP (par exemple sous Apache en créant un fichier ./p/i/.htaccess et .htpasswd)
* le nom dutilisateur HTTP doit correspondre au nom dutilisateur FreshRSS
* par Mozilla Persona, en renseignant ladresse courriel des utilisateurs
* Installateur supportant les mises à jour :
* Depuis une v0.6, placer application.ini et Configuration.array.php dans le nouveau répertoire “./data/”
(voir réorganisation ci-dessous)
* Pour les versions suivantes, juste garder le répertoire “./data/”
* Rafraîchissement automatique du nombre darticles non lus toutes les deux minutes (utilise le cache HTTP à bon escient)
* Permet aussi de conserver la session valide, surtout dans le cas de Persona
* Nouvelle page de statistiques (nombres darticles par jour / catégorie)
* Importation OPML instantanée et plus tolérante
* Nouvelle gestion des favicons avec téléchargement en parallèle
* Nouvelles options
* Réorganisation des options
* Gestion des utilisateurs
* Améliorations partage vers Shaarli, Poche, Diaspora*, Facebook, Twitter, Google+, courriel
* Raccourci s par défaut
* Permet la suppression de tous les articles dun flux
* Option pour marquer les articles comme lus dès la réception
* Permet de configurer plus finement le nombre darticles minimum à conserver par flux
* Permet de modifier la description et ladresse dun flux RSS ainsi que le site Web associé
* Nouveau raccourci pour ouvrir/fermer un article (c par défaut)
* Boutons pour effacer les logs et pour purger les vieux articles
* Nouveaux filtres daffichage : seulement les articles favoris, et seulement les articles lus
* SQL :
* Nouveau moteur de recherche, aussi accessible depuis la vue mobile
* Mots clefs de recherche “intitle:”, “inurl:”, “author:”
* Les articles sont triés selon la date de leur ajout dans FreshRSS plutôt que la date déclarée (souvent erronée)
* Permet de marquer tout comme lu sans affecter les nouveaux articles arrivés en cours de lecture
* Permet une pagination efficace
* Refactorisation
* Les tables sont préfixées avec le nom dutilisateur afin de permettre le mode multi-utilisateurs
* Amélioration des performances
* Tolère un beaucoup plus grand nombre darticles
* Compression des données côté MySQL plutôt que côté PHP
* Incompatible avec la version 0.6 (nécessite une mise à jour grâce à linstallateur)
* Affichage de la taille de la base de données dans FreshRSS
* Correction problème de marquage de tous les favoris comme lus
* HTML5 :
* Support des balises HTML5 audio, video, et éléments associés
* Utilisation de preload="none", et réécriture correcte des adresses, aussi en HTTPS
* Protection HTML5 des iframe (sandbox="allow-scripts allow-same-origin")
* Filtrage des object et embed
* Chargement différé HTML5 (postpone="") pour iframe et video
* Chargement différé JavaScript pour iframe
* CSS :
* Nouveau thème sombre
* Chargement plus robuste des thèmes
* Meilleur support des longs titres darticles sur des écrans étroits
* Meilleure accessibilité
* FreshRSS fonctionne aussi en mode dégradé sans images (alternatives Unicode) et/ou sans CSS
* Diverses améliorations
* PHP :
* Encore plus tolérant pour les flux comportant des erreurs
* Mise à jour automatique de lURL du flux (en base de données) lorsque SimplePie découvre quelle a changé
* Meilleure gestion des caractères spéciaux dans différents cas
* Compatibilité PHP 5.5+ avec OPcache
* Amélioration des performances
* Chargement automatique des classes
* Alternative dans le cas dabsence de librairie JSON
* Pour le développement, le cache HTTP peut être désactivé en créant un fichier “./data/no-cache.txt”
* Réorganisation des fichiers et répertoires, en particulier :
* Tous les fichiers utilisateur sont dans “./data/” (y compris “cache”, “favicons”, et “log”)
* Déplacement de “./app/configuration/application.ini” vers “./data/config.php”
* Meilleure sécurité et compatibilité
* Déplacement de “./public/data/Configuration.array.php” vers “./data/*_user.php”
* Déplacement de “./public/” vers “./p/”
* Déplacement de “./public/index.php” vers “./p/i/index.php” (voir cookie ci-dessous)
* Déplacement de “./actualize_script.php” vers “./app/actualize_script.php” (pour une meilleure sécurité)
* Pensez à mettre à jour votre Cron !
* Divers :
* Nouvelle politique de cookie de session (témoin de connexion)
* Utilise un nom poli “FreshRSS” (évite des problèmes avec certains filtres)
* Se limite au répertoire “./FreshRSS/p/i/” pour de meilleures performances HTTP
* Les images, CSS, scripts sont servis sans cookie
* Utilise “HttpOnly” pour plus de sécurité
* Nouvel “agent utilisateur” exposé lors du téléchargement des flux, par exemple :
* “FreshRSS/0.7 (Linux; http://freshrss.org) SimplePie/1.3.1”
* Script dactualisation avec plus de messages
* Sur la sortie standard, ainsi que dans le log système (syslog)
* Affichage du numéro de version dans "À propos"
## 2013-11-21 FreshRSS 0.6.1
* Corrige bug chargement du JavaScript
* Affiche un message derreur plus explicite si fichier de configuration inaccessible
## 2013-11-17 FreshRSS 0.6
* Nettoyage du code JavaScript + optimisations
* Utilisation dadresses relatives
* Amélioration des performances coté client
* Mise à jour automatique du nombre darticles non lus
* Corrections traductions
* Mise en cache de FreshRSS
* Amélioration des retours utilisateur lorsque la configuration nest pas bonne
* Actualisation des flux après une importation OPML
* Meilleure prise en charge des flux RSS invalides
* Amélioration de la vue globale
* Possibilité de personnaliser les icônes de lecture
* Suppression de champs lors de linstallation (base_url et sel)
* Correction bugs divers
## 2013-10-15 FreshRSS 0.5.1
* Correction bug des catégories disparues
* Correction traduction i18n/fr et i18n/en
* Suppression de certains appels à la feuille de style fallback.css
## 2013-10-12 FreshRSS 0.5.0
* Possibilité dinterdire la lecture anonyme
* Option pour garder lhistorique dun flux
* Lors dun clic sur “Marquer tous les articles comme lus”, FreshRSS peut désormais sauter à la prochaine catégorie / prochain flux avec des articles non lus.
* Ajout dun token pour accéder aux flux RSS générés par FreshRSS sans nécessiter de connexion
* Possibilité de partager vers Facebook, Twitter et Google+
* Possibilité de changer de thème
* Le menu de navigation (article précédent / suivant / haut de page) a été ajouté à la vue non mobile
* La police OpenSans est désormais appliquée
* Amélioration de la page de configuration
* Une meilleure sortie pour limprimante
* Quelques retouches du design par défaut
* Les vidéos ne dépassent plus du cadre de lécran
* Nouveau logo
* Possibilité dajouter un préfixe aux tables lors de linstallation
* Ajout dun champ en base de données keep_history à la table feed
* Si possible, création automatique de la base de données si elle nexiste pas lors de linstallation
* Lutilisation dUTF-8 est forcée
* Le marquage automatique au défilement de la page a été amélioré
* La vue globale a été énormément améliorée et est beaucoup plus utile
* Amélioration des requêtes SQL
* Amélioration du JavaScript
* Correction bugs divers
## 2013-07-02 FreshRSS 0.4.0
* Correction bug et ajout notification lors de la phase dinstallation
* Affichage derreur si fichier OPML invalide
* Les tags sont maintenant cliquables pour filtrer dessus
* Amélioration vue mobile (boutons plus gros et ajout dune barre de navigation)
* Possibilité dajouter directement un flux dans une catégorie dès son ajout
* Affichage des flux en erreur (injoignable par exemple) en rouge pour les différencier
* Possibilité de changer les noms des flux
* Ajout dune option (désactivable donc) pour charger les images en lazyload permettant de ne pas charger toutes les images dun coup
* Le framework Minz est maintenant directement inclus dans larchive (plus besoin de passer par ./build.sh)
* Amélioration des performances pour la récupération des flux tronqués
* Possibilité dimporter des flux sans catégorie lors de limport OPML
* Suppression de “lAPI” (qui était de toute façon très basique) et de la fonctionnalité de “notes”
* Amélioration de la recherche (garde en mémoire si lon a sélectionné une catégorie) par exemple
* Modification apparence des balises hr et pre
* Meilleure vérification des champs de formulaire
* Remise en place du mode “endless” (permettant de simplement charger les articles qui suivent plutôt que de charger une nouvelle page)
* Ajout dune page de visualisation des logs
* Ajout dune option pour optimiser la BDD (diminue sa taille)
* Ajout des vues lecture et globale (assez basique)
* Les vidéos YouTube ne débordent plus du cadre sur les petits écrans
* Ajout dune option pour marquer les articles comme lus lors du défilement (et suppression de celle au chargement de la page)
## 2013-05-05 FreshRSS 0.3.0
* Fallback pour les icônes SVG (utilisation de PNG à la place)
* Fallback pour les propriétés CSS3 (utilisation de préfixes)
* Affichage des tags associés aux articles
* Internationalisation de lapplication (gestion des langues anglaise et française)
* Gestion des flux protégés par authentification HTTP
* Mise en cache des favicons
* Création dun logo *temporaire*
* Affichage des vidéos dans les articles
* Gestion de la recherche et filtre par tags pleinement fonctionnels
* Création dun vrai script CRON permettant de mettre tous les flux à jour
* Correction bugs divers
## 2013-04-17 FreshRSS 0.2.0
* Création dun installateur
* Actualisation des flux en Ajax
* Partage par mail et Shaarli ajouté
* Export par flux RSS
* Possibilité de vider une catégorie
* Possibilité de sélectionner les catégories en vue mobile
* Les flux peuvent être sortis du flux principal (système de priorité)
* Amélioration ajout / import / export des flux
* Amélioration actualisation (meilleure gestion des erreurs)
* Améliorations CSS
* Changements dans la base de données
* Màj de la librairie SimplePie
* Flux sans auteurs gérés normalement
* Correction bugs divers
## 2013-04-08 FreshRSS 0.1.0
* “Première” version

57
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,57 @@
# How to contribute to FreshRSS?
## Join us on the mailing lists
Do you want to ask us some questions? Do you want to discuss with us? Don't hesitate to subscribe to our mailing lists!
- The first mailing is destined to generic information, it should be adapted to users. [Join mailing@freshrss.org](https://freshrss.org/mailman/listinfo/mailing).
- The second mailing is mainly for developers. [Join dev@freshrss.org](https://freshrss.org/mailman/listinfo/dev)
## Report a bug
You found a bug? Don't panic, here are some steps to report it easily:
1. Search for it on [the bug tracker](https://github.com/FreshRSS/FreshRSS/issues) (don't forget to use the search bar).
2. If you find a similar bug, don't hesitate to post a comment to add more importance to the related ticket.
3. If you didn't find it, [open a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new).
If you have to create a new ticket, try to apply the following advices:
- Give an explicit title to the ticket so it will be easier to find it later.
- Be as exhaustive as possible in the description: what did you do? What is the bug? What are the steps to reproduce the bug?
- We also need some information:
+ Your FreshRSS version (on about page or `constants.php` file)
+ Your server configuration: type of hosting, PHP version
+ Your storage system (MySQL / MariaDB or SQLite)
+ If possible, the related logs (PHP logs and FreshRSS logs under `data/users/your_user/log.txt`)
## Fix a bug
Did you want to fix a bug? To keep a great coordination between collaborators, you will have to follow these indications:
1. Be sure the bug is associated to a ticket and say you work on it.
2. [Fork this project repository](https://help.github.com/articles/fork-a-repo/).
3. [Create a new branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/). The name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783).
4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **dev branch**.
If you have to write code, please follow [our coding style recommendations](http://doc2.freshrss.org/en/Developer_documentation/First_steps/Coding_style).
**Tip:** if you are searching for bugs easy to fix, have a look at the « [New comers](https://github.com/FreshRSS/FreshRSS/labels/New%20comers) » ticket label.
## Submit an idea
You have great ideas, yes! Don't be shy and open [a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new) on our bug tracker to ask if we can implement it. The greatest ideas often come from the shyest suggestions!
If your idea is nice, we'll have a look at it.
## Contribute to internationalization (i18n)
If you want to improve internationalization, please open a new ticket first and follow indications from « Fix a bug » section.
Translations are present in the subdirectories of `./app/i18n/`.
We are working on a better way to handle internationalization but don't hesitate to suggest any idea!
## Contribute to documentation
The documentation needs a lot of improvements in order to be more useful to new contributors and we are working on it. If you want to give some help, meet us on [the dedicated repository](https://github.com/FreshRSS/documentation)!

37
CREDITS.md Normal file
View File

@@ -0,0 +1,37 @@
This is a credit file of people who have [contributed to FreshRSS](https://github.com/FreshRSS/FreshRSS/graphs/contributors) with, at least,
one commit on one of the FreshRSS repositories (at https://github.com/FreshRSS).
Please note a commit on THIS specific file is not considered as a contribution
(too easy!). Its purpose is to show that even the smallest contribution is important.
People are sorted by name so please keep this order.
---
* [Alexandre Alapetite](https://github.com/Alkarex): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Alkarex), [Web](http://alexandre.alapetite.fr/)
* [Alexis Degrugillier](https://github.com/aledeg): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=aledeg)
* [Alwaysin](https://github.com/Alwaysin): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Alwaysin)
* [Amaury Carrade](https://github.com/AmauryCarrade): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=AmauryCarrade), [Web](https://amaury.carrade.eu/)
* [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ASMfreaK)
* [Damstre](https://github.com/Damstre): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Damstre),
* [danc](https://github.com/danc): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=danc), [Web](http://tintouli.free.fr/)
* [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ealdraed)
* [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Frenzie), [Web](http://fransdejonge.com/)
* [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
* [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb)
* [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=edhelas), [Web](http://edhelas.movim.eu/)
* [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/)
* [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=kevinpapst), [Web](http://www.kevinpapst.de/)
* [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ldidry), [Web](https://www.fiat-tux.fr/)
* [marcomrc](https://github.com/marcomrc): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=marcomrc)
* [Marcus Rohrmoser](https://github.com/mro): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=mro), [Web](http://mro.name/~me)
* [Marien Fressinaud](https://github.com/marienfressinaud): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=marienfressinaud), [Web](http://marienfressinaud.fr/)
* [Melvyn Laïly](https://github.com/yaurthek): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=yaurthek), [Web](http://x2a.yt/)
* [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicolaselie)
* [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
* [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
* [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)
* [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/)
* [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi)
* [subic](https://github.com/subic): [contributions](https://github.com/FreshRSS/documentation/commits?author=subic)
* [Tets42](https://github.com/Tets42): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Tets42)
* [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue)
* [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo)

179
README.fr.md Normal file
View File

@@ -0,0 +1,179 @@
* [English version](README.md)
# FreshRSS
FreshRSS est un agrégateur de flux RSS à auto-héberger à limage 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 dun mode de lecture anonyme.
Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des notifications instantanées depuis les sites compatibles.
Il y a une API pour les clients (mobiles), ainsi quune [interface en ligne de commande](./cli/README.md).
Enfin, il permet lajout d[extensions](#extensions) pour encore plus de personnalisation.
* Site officiel : http://freshrss.org
* Démo : http://demo.freshrss.org/
* Licence : [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
![Logo de FreshRSS](./doc/FreshRSS-logo.png)
# Téléchargement
Voir la [liste des versions](../../releases).
## À propos des branches
* Utilisez [la branche master](https://github.com/FreshRSS/FreshRSS/tree/master/) si vous visez la stabilité.
* Pour ceux qui veulent bien aider à tester ou déveloper les dernières fonctionnalités, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras !
# Avertissements
Cette application a été développée pour sadapter principalement à des besoins personnels, et aucune garantie nest fournie.
Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
Nous sommes une communauté amicale.
# Prérequis
* Serveur modeste, par exemple sous Linux ou Windows
* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
* Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
* PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour dencore meilleures performances)
* Requis : [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), et [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql)
* Recommandés : [JSON](http://php.net/json), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion dencodages), [ZIP](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL (experimental)
* Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
* Fonctionne aussi sur mobile
![Capture décran de FreshRSS](./doc/FreshRSS-screenshot.png)
# Installation
1. Récupérez lapplication FreshRSS via la commande git ou [en téléchargeant larchive](../releases)
2. Placez lapplication 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 dinstallation
* ou utilisez [linterface en ligne de commande](./cli/README.md)
5. Tout devrait fonctionner :) En cas de problème, nhésitez pas à [nous contacter](https://github.com/FreshRSS/FreshRSS/issues).
6. Des paramètres de configuration avancée peuvent être accédés depuis [config.php](./data/config.default.php).
## Installation automatisée
* [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
* [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
## Exemple dinstallation complète sur Linux Debian/Ubuntu
```sh
# Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
sudo apt-get install apache2
sudo a2enmod headers expires rewrite ssl #Modules Apache
# Pour Ubuntu <= 15.10, Debian <= 8 Jessie
sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
sudo apt-get install libapache2-mod-php5 #Pour Apache
sudo apt-get install mysql-server mysql-client php5-mysql #Base de données MySQL optionnelle
sudo apt-get install postgresql php5-pgsql #Base de données PostgreSQL optionnelle
# Pour Ubuntu >= 16.04, Debian >= 9 Stretch
sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
sudo apt install libapache2-mod-php #Pour Apache
sudo apt install mysql-server mysql-client php-mysql #Base de données MySQL optionnelle
sudo apt install postgresql php-pgsql #Base de données PostgreSQL optionnelle
## Redémarrage du serveur Web
sudo service apache2 restart
# Pour FreshRSS lui-même (git est optionnel si vous déployez manuellement les fichiers dinstallation)
cd /usr/share/
sudo apt-get install git
sudo git clone https://github.com/FreshRSS/FreshRSS.git
cd FreshRSS
# Si vous souhaitez utiliser la branche développement de FreshRSS
sudo git checkout -b dev origin/dev
# Mettre les droits daccès pour le serveur Web
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
# Si vous souhaitez permettre les mises à jour par linterface Web
sudo chmod -R g+w .
# Publier FreshRSS dans votre répertoire HTML public
sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
# Naviguez vers http://example.net/FreshRSS pour terminer linstallation
# (Si vous le faite depuis localhost, vous pourrez avoir à ajuster le réglage de votre adresse publique)
# ou utilisez linterface en ligne de commande
# Mettre à jour FreshRSS vers une nouvelle version par git
cd /usr/share/FreshRSS
sudo git pull
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
```
## Contrôle daccès
Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter laccès à votre FreshRSS. Au choix :
* En utilisant lidentification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé fonctionne avec certaines versions de PHP 5.3.3+)
* En utilisant un contrôle daccès HTTP défini par votre serveur Web
* Voir par exemple la [documentation dApache sur lauthentification](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 dactualisation automatique des flux.
Consultez la documentation de Cron de votre système dexploitation ([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)…).
Cest une bonne idée dutiliser le même utilisateur que votre serveur Web (souvent “www-data”).
Par exemple, pour exécuter le script toutes les heures :
```
8 * * * * php /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
### Exemple pour Debian / Ubuntu
Créer `/etc/cron.d/FreshRSS` avec :
```
7,37 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
# Conseils
* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
* Le fichier `./constants.php` définit les chemins daccès aux répertoires clés de lapplication. Si vous les bougez, tout se passe ici.
* En cas de problème, les logs peuvent être utile à lire, soit depuis linterface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
# Sauvegarde
* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/*_user.php`
* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
* soit depuis linterface Web, soit [en ligne de commande](./cli/README.md)
* Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
```bash
mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
```
# Extensions
FreshRSS permet lajout dextensions en plus des fonctionnalités natives.
Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensions).
# 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/)
* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon)
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
* [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/)
* [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)
# Clients compatibles
Tout client supportant une API de type Google Reader. Sélection :
* Android
* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) avec [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Propriétaire)
* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, F-Droid)
* Linux
* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)

203
README.md
View File

@@ -1,92 +1,179 @@
* [Version française](README.fr.md)
# FreshRSS
FreshRSS est un agrégateur de flux RSS à auto-héberger à limage de [Leed](http://projet.idleman.fr/leed/) ou de [Kriss Feed](http://tontof.net/kriss/feed/).
FreshRSS is a self-hosted RSS feed aggregator such as [Leed](http://projet.idleman.fr/leed/) or [Kriss Feed](http://tontof.net/kriss/feed/).
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 lightweight, easy to work with, powerful and customizable.
Il permet de gérer plusieurs utilisateurs, et dispose dun mode de lecture anonyme.
It is a multi-user application with an anonymous reading mode.
It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant notifications from compatible Web sites.
There is an API for (mobile) clients, and a [Command-Line Interface](./cli/README.md).
Finally, it supports [extensions](#extensions) for further tuning.
* Site officiel : http://freshrss.org
* Démo : http://demo.freshrss.org/
* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
* Version actuelle : 0.7.1
* Date de publication 2014-02-19
* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
* Official website: http://freshrss.org
* Demo: http://demo.freshrss.org/
* 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](./doc/FreshRSS-logo.png)
# Releases
See the [list of releases](../../releases).
## About branches
* Use [the master branch](https://github.com/FreshRSS/FreshRSS/tree/master/) if you need a stable version.
* For those willing to help testing or developing the latest features, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you!
# Disclaimer
Cette application a été développée pour sadapter à des besoins personnels et non professionnels.
Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement.
Je mengage 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 fulfil personal needs primarily, and comes with absolutely no warranty.
Feature requests, bug reports, and other contributions are welcome. The best way is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
We are a friendly community.
# 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)
* MySQL 5.0.3+ (ou SQLite 3.7.4+ à venir)
* 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 1 with response time under a second (tested with 150 feeds, 22k articles)
* A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
* PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance)
* Required extensions: [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), and [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql)
* Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [ZIP](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL (experimental)
* A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
* Works on mobile
![Capture décran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
![FreshRSS screenshot](./doc/FreshRSS-screenshot.png)
# Installation
1. Récupérez lapplication FreshRSS via la commande git ou [en téléchargeant larchive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
2. Placez lapplication 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 dinstallation
5. Tout devrait fonctionner :) En cas de problème, nhésitez pas à me contacter.
1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip)
2. Dump the application on your server (expose only the `./p/` folder)
3. Add write access on `./data/` folder to the webserver user
4. Access FreshRSS with your browser and follow the installation process
* or use the [Command-Line Interface](./cli/README.md)
5. Everything should be working :) If you encounter any problem, feel free [contact us](https://github.com/FreshRSS/FreshRSS/issues).
6. Advanced configuration settings can be seen in [config.php](./data/config.default.php).
# Contrôle daccès
Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter laccès à votre FreshRSS :
* En utilisant lidentification par [Mozilla Persona](https://login.persona.org/about) incluse dans FreshRSS
* En utilisant un contrôle daccès HTTP défini par votre serveur Web
* Voir par exemple la [documentation dApache sur lauthentification](http://httpd.apache.org/docs/trunk/howto/auth.html)
* Créer dans ce cas un fichier `./p/i/.htaccess` avec un fichier `.htpasswd` correspondant.
## Automated install
* [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
* [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
# Rafraîchissement automatique des flux
* Vous pouvez ajouter une tâche Cron lançant régulièrement le script dactualisation automatique des flux.
Consultez la documentation de Cron de votre système dexploitation ([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)…).
Cest une bonne idée dutiliser le même utilisateur que votre serveur Web (souvent “www-data”).
Par exemple, pour exécuter le script toutes les heures :
## Example of full installation on Linux Debian/Ubuntu
```sh
# If you use an Apache Web server (otherwise you need another Web server)
sudo apt-get install apache2
sudo a2enmod headers expires rewrite ssl #Apache modules
```
7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
# For Ubuntu <= 15.10, Debian <= 8 Jessie
sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
sudo apt-get install libapache2-mod-php5 #For Apache
sudo apt-get install mysql-server mysql-client php5-mysql #Optional MySQL database
sudo apt-get install postgresql php5-pgsql #Optional PostgreSQL database
# For Ubuntu >= 16.04, Debian >= 9 Stretch
sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
sudo apt install libapache2-mod-php #For Apache
sudo apt install mysql-server mysql-client php-mysql #Optional MySQL database
sudo apt install postgresql php-pgsql #Optional PostgreSQL database
# Restart Web server
sudo service apache2 restart
# For FreshRSS itself (git is optional if you manually download the installation files)
cd /usr/share/
sudo apt-get install git
sudo git clone https://github.com/FreshRSS/FreshRSS.git
cd FreshRSS
# If you want to use the development version of FreshRSS
sudo git checkout -b dev origin/dev
# Set the rights so that your Web server can access the files
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
# If you would like to allow updates from the Web interface
sudo chmod -R g+w .
# Publish FreshRSS in your public HTML directory
sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
# Navigate to http://example.net/FreshRSS to complete the installation
# (If you do it from localhost, you may have to adjust the setting of your public address later)
# or use the Command-Line Interface
# Update to a newer version of FreshRSS with git
cd /usr/share/FreshRSS
sudo git pull
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
```
# 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 daccès aux répertoires clés de lapplication. Si vous les bougez, tout se passe ici.
* En cas de problème, les logs peuvent être utile à lire, soit depuis linterface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
## Access control
It is needed for the multi-user mode to limit access to FreshRSS. You can:
* use form authentication (need JavaScript and PHP 5.3.7+, works with some PHP 5.3.3+)
* use HTTP authentication supported by your web server
* See [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html)
* In that case, create a `./p/i/.htaccess` file with a matching `.htpasswd` file.
# 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 :
## 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 is a good idea to use the Web server user.
For instance, if you want to run the script every hour:
```
9 * * * * php /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
### Example on Debian / Ubuntu
Create `/etc/cron.d/FreshRSS` with:
```
6,36 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
# Advices
* For a better security, expose only the `./p/` folder on the web.
* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
* The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here.
* If you encounter any problem, logs are accessible from the interface or manually in `./data/log/*.log` files.
# Backup
* You need to keep `./data/config.php`, and `./data/*_user.php` files
* You can export your feed list in OPML format from FreshRSS
* either from the Web interface, or from the [Command-Line Interface](./cli/README.md)
* To save articles, you can use [phpMyAdmin](http://www.phpmyadmin.net) or MySQL tools:
```bash
mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
mysqldump -u user -p --databases freshrss > freshrss.sql
```
# Bibliothèques incluses
# Extensions
FreshRSS supports further customizations by adding extensions on top of its core functionality.
See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions).
# Included libraries
* [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/)
* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon)
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
* [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/)
* [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/)
* [Lazy Load](http://www.appelsiini.net/projects/lazyload)
## 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)
# Compatible clients
Any client supporting a Google Reader-like API. Selection:
* Android
* [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Closed source)
* [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, F-Droid)
* Linux
* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)

View File

@@ -0,0 +1,212 @@
<?php
/**
* This controller handles action about authentication.
*/
class FreshRSS_auth_Controller extends Minz_ActionController {
/**
* This action handles authentication management page.
*
* Parameters are:
* - token (default: current token)
* - anon_access (default: false)
* - anon_refresh (default: false)
* - auth_type (default: none)
* - unsafe_autologin (default: false)
* - api_enabled (default: false)
*
* @todo move unsafe_autologin in an extension.
*/
public function indexAction() {
if (!FreshRSS_Auth::hasAccess('admin')) {
Minz_Error::error(403);
}
Minz_View::prependTitle(_t('admin.auth.title') . ' · ');
if (Minz_Request::isPost()) {
$ok = true;
$current_token = FreshRSS_Context::$user_conf->token;
$token = Minz_Request::param('token', $current_token);
FreshRSS_Context::$user_conf->token = $token;
$ok &= FreshRSS_Context::$user_conf->save();
$anon = Minz_Request::param('anon_access', false);
$anon = ((bool)$anon) && ($anon !== 'no');
$anon_refresh = Minz_Request::param('anon_refresh', false);
$anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no');
$auth_type = Minz_Request::param('auth_type', 'none');
$unsafe_autologin = Minz_Request::param('unsafe_autologin', false);
$api_enabled = Minz_Request::param('api_enabled', false);
if ($anon != FreshRSS_Context::$system_conf->allow_anonymous ||
$auth_type != FreshRSS_Context::$system_conf->auth_type ||
$anon_refresh != FreshRSS_Context::$system_conf->allow_anonymous_refresh ||
$unsafe_autologin != FreshRSS_Context::$system_conf->unsafe_autologin_enabled ||
$api_enabled != FreshRSS_Context::$system_conf->api_enabled) {
// TODO: test values from form
FreshRSS_Context::$system_conf->auth_type = $auth_type;
FreshRSS_Context::$system_conf->allow_anonymous = $anon;
FreshRSS_Context::$system_conf->allow_anonymous_refresh = $anon_refresh;
FreshRSS_Context::$system_conf->unsafe_autologin_enabled = $unsafe_autologin;
FreshRSS_Context::$system_conf->api_enabled = $api_enabled;
$ok &= FreshRSS_Context::$system_conf->save();
}
invalidateHttpCache();
if ($ok) {
Minz_Request::good(_t('feedback.conf.updated'),
array('c' => 'auth', 'a' => 'index'));
} else {
Minz_Request::bad(_t('feedback.conf.error'),
array('c' => 'auth', 'a' => 'index'));
}
}
}
/**
* This action handles the login page.
*
* It forwards to the correct login page (form) or main page if
* the user is already connected.
*/
public function loginAction() {
if (FreshRSS_Auth::hasAccess()) {
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
}
$auth_type = FreshRSS_Context::$system_conf->auth_type;
switch ($auth_type) {
case 'form':
Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin'));
break;
case 'http_auth':
case 'none':
// It should not happened!
Minz_Error::error(404);
default:
// TODO load plugin instead
Minz_Error::error(404);
}
}
/**
* This action handles form login page.
*
* If this action is reached through a POST request, username and password
* are compared to login the current user.
*
* Parameters are:
* - nonce (default: false)
* - username (default: '')
* - challenge (default: '')
* - keep_logged_in (default: false)
*
* @todo move unsafe autologin in an extension.
*/
public function formLoginAction() {
invalidateHttpCache();
$file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js');
Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime));
$conf = Minz_Configuration::get('system');
$limits = $conf->limits;
$this->view->cookie_days = round($limits['cookie_duration'] / 86400, 1);
if (Minz_Request::isPost()) {
$nonce = Minz_Session::param('nonce');
$username = Minz_Request::param('username', '');
$challenge = Minz_Request::param('challenge', '');
$conf = get_user_configuration($username);
if (is_null($conf)) {
Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
return;
}
$ok = FreshRSS_FormAuth::checkCredentials(
$username, $conf->passwordHash, $nonce, $challenge
);
if ($ok) {
// Set session parameter to give access to the user.
Minz_Session::_param('currentUser', $username);
Minz_Session::_param('passwordHash', $conf->passwordHash);
FreshRSS_Auth::giveAccess();
// Set cookie parameter if nedded.
if (Minz_Request::param('keep_logged_in')) {
FreshRSS_FormAuth::makeCookie($username, $conf->passwordHash);
} else {
FreshRSS_FormAuth::deleteCookie();
}
// All is good, go back to the index.
Minz_Request::good(_t('feedback.auth.login.success'),
array('c' => 'index', 'a' => 'index'));
} else {
Minz_Log::warning('Password mismatch for' .
' user=' . $username .
', nonce=' . $nonce .
', c=' . $challenge);
Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
}
} elseif (FreshRSS_Context::$system_conf->unsafe_autologin_enabled) {
$username = Minz_Request::param('u', '');
$password = Minz_Request::param('p', '');
Minz_Request::_param('p');
if (!$username) {
return;
}
$conf = get_user_configuration($username);
if (is_null($conf)) {
return;
}
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
$s = $conf->passwordHash;
$ok = password_verify($password, $s);
unset($password);
if ($ok) {
Minz_Session::_param('currentUser', $username);
Minz_Session::_param('passwordHash', $s);
FreshRSS_Auth::giveAccess();
Minz_Request::good(_t('feedback.auth.login.success'),
array('c' => 'index', 'a' => 'index'));
} else {
Minz_Log::warning('Unsafe password mismatch for user ' . $username);
Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
}
}
}
/**
* This action removes all accesses of the current user.
*/
public function logoutAction() {
invalidateHttpCache();
FreshRSS_Auth::removeAccess();
Minz_Request::good(_t('feedback.auth.logout.success'),
array('c' => 'index', 'a' => 'index'));
}
/**
* This action gives possibility to a user to create an account.
*/
public function registerAction() {
if (max_registrations_reached()) {
Minz_Error::error(403);
}
Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
* Controller to handle actions relative to categories.
* User needs to be connected.
*/
class FreshRSS_category_Controller extends Minz_ActionController {
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
$catDAO = new FreshRSS_CategoryDAO();
$catDAO->checkDefault();
}
/**
* This action creates a new category.
*
* Request parameter is:
* - new-category
*/
public function createAction() {
$catDAO = new FreshRSS_CategoryDAO();
$url_redirect = array('c' => 'subscription', 'a' => 'index');
$limits = FreshRSS_Context::$system_conf->limits;
$this->view->categories = $catDAO->listCategories(false);
if (count($this->view->categories) >= $limits['max_categories']) {
Minz_Request::bad(_t('feedback.sub.category.over_max', $limits['max_categories']),
$url_redirect);
}
if (Minz_Request::isPost()) {
invalidateHttpCache();
$cat_name = Minz_Request::param('new-category');
if (!$cat_name) {
Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect);
}
$cat = new FreshRSS_Category($cat_name);
if ($catDAO->searchByName($cat->name()) != null) {
Minz_Request::bad(_t('feedback.sub.category.name_exists'), $url_redirect);
}
$values = array(
'id' => $cat->id(),
'name' => $cat->name(),
);
if ($catDAO->addCategory($values)) {
Minz_Request::good(_t('feedback.sub.category.created', $cat->name()), $url_redirect);
} else {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
}
Minz_Request::forward($url_redirect, true);
}
/**
* This action updates the given category.
*
* Request parameters are:
* - id
* - name
*/
public function updateAction() {
$catDAO = new FreshRSS_CategoryDAO();
$url_redirect = array('c' => 'subscription', 'a' => 'index');
if (Minz_Request::isPost()) {
invalidateHttpCache();
$id = Minz_Request::param('id');
$name = Minz_Request::param('name', '');
if (strlen($name) <= 0) {
Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect);
}
if ($catDAO->searchById($id) == null) {
Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect);
}
$cat = new FreshRSS_Category($name);
$values = array(
'name' => $cat->name(),
);
if ($catDAO->updateCategory($id, $values)) {
Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
} else {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
}
Minz_Request::forward($url_redirect, true);
}
/**
* This action deletes a category.
* Feeds in the given category are moved in the default category.
* Related user queries are deleted too.
*
* Request parameter is:
* - id (of a category)
*/
public function deleteAction() {
$feedDAO = FreshRSS_Factory::createFeedDao();
$catDAO = new FreshRSS_CategoryDAO();
$url_redirect = array('c' => 'subscription', 'a' => 'index');
if (Minz_Request::isPost()) {
invalidateHttpCache();
$id = Minz_Request::param('id');
if (!$id) {
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
}
if ($id === FreshRSS_CategoryDAO::defaultCategoryId) {
Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect);
}
if ($feedDAO->changeCategory($id, FreshRSS_CategoryDAO::defaultCategoryId) === false) {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
if ($catDAO->deleteCategory($id) === false) {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
// Remove related queries.
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
'c_' . $id, FreshRSS_Context::$user_conf->queries);
FreshRSS_Context::$user_conf->save();
Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect);
}
Minz_Request::forward($url_redirect, true);
}
/**
* This action deletes all the feeds relative to a given category.
* Feed-related queries are deleted.
*
* Request parameter is:
* - id (of a category)
*/
public function emptyAction() {
$feedDAO = FreshRSS_Factory::createFeedDao();
$url_redirect = array('c' => 'subscription', 'a' => 'index');
if (Minz_Request::isPost()) {
invalidateHttpCache();
$id = Minz_Request::param('id');
if (!$id) {
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
}
// List feeds to remove then related user queries.
$feeds = $feedDAO->listByCategory($id);
if ($feedDAO->deleteFeedByCategory($id)) {
// TODO: Delete old favicons
// Remove related queries
foreach ($feeds as $feed) {
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
'f_' . $feed->id(), FreshRSS_Context::$user_conf->queries);
}
FreshRSS_Context::$user_conf->save();
Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect);
} else {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
}
Minz_Request::forward($url_redirect, true);
}
}

View File

@@ -1,300 +1,180 @@
<?php
/**
* Controller to handle every configuration options.
*/
class FreshRSS_configure_Controller extends Minz_ActionController {
public function firstAction () {
if (!$this->view->loginOk) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
}
$catDAO = new FreshRSS_CategoryDAO ();
$catDAO->checkDefault ();
}
public function categorizeAction () {
$feedDAO = new FreshRSS_FeedDAO ();
$catDAO = new FreshRSS_CategoryDAO ();
$defaultCategory = $catDAO->getDefault ();
$defaultId = $defaultCategory->id ();
if (Minz_Request::isPost ()) {
$cats = Minz_Request::param ('categories', array ());
$ids = Minz_Request::param ('ids', array ());
$newCat = trim (Minz_Request::param ('new_category', ''));
foreach ($cats as $key => $name) {
if (strlen ($name) > 0) {
$cat = new FreshRSS_Category ($name);
$values = array (
'name' => $cat->name (),
);
$catDAO->updateCategory ($ids[$key], $values);
} elseif ($ids[$key] != $defaultId) {
$feedDAO->changeCategory ($ids[$key], $defaultId);
$catDAO->deleteCategory ($ids[$key]);
}
}
if ($newCat != '') {
$cat = new FreshRSS_Category ($newCat);
$values = array (
'id' => $cat->id (),
'name' => $cat->name (),
);
if ($catDAO->searchByName ($newCat) == false) {
$catDAO->addCategory ($values);
}
}
invalidateHttpCache();
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('categories_updated')
);
Minz_Session::_param ('notification', $notif);
Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
}
$this->view->categories = $catDAO->listCategories (false);
$this->view->defaultCategory = $catDAO->getDefault ();
$this->view->feeds = $feedDAO->listFeeds ();
$this->view->flux = false;
Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' · ');
}
public function feedAction () {
$catDAO = new FreshRSS_CategoryDAO ();
$this->view->categories = $catDAO->listCategories (false);
$feedDAO = new FreshRSS_FeedDAO ();
$this->view->feeds = $feedDAO->listFeeds ();
$id = Minz_Request::param ('id');
if ($id == false && !empty ($this->view->feeds)) {
$id = current ($this->view->feeds)->id ();
}
$this->view->flux = false;
if ($id != false) {
$this->view->flux = $this->view->feeds[$id];
if (!$this->view->flux) {
Minz_Error::error (
404,
array ('error' => array (Minz_Translate::t ('page_not_found')))
);
} else {
if (Minz_Request::isPost () && $this->view->flux) {
$user = Minz_Request::param ('http_user', '');
$pass = Minz_Request::param ('http_pass', '');
$httpAuth = '';
if ($user != '' || $pass != '') {
$httpAuth = $user . ':' . $pass;
}
$cat = intval(Minz_Request::param('category', 0));
$values = array (
'name' => Minz_Request::param ('name', ''),
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
'website' => Minz_Request::param('website', ''),
'url' => Minz_Request::param('url', ''),
'category' => $cat,
'pathEntries' => Minz_Request::param ('path_entries', ''),
'priority' => intval(Minz_Request::param ('priority', 0)),
'httpAuth' => $httpAuth,
'keep_history' => intval(Minz_Request::param ('keep_history', -2)),
);
if ($feedDAO->updateFeed ($id, $values)) {
$this->view->flux->_category ($cat);
$this->view->flux->faviconPrepare();
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feed_updated')
);
} else {
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('error_occurred_update')
);
}
invalidateHttpCache();
Minz_Session::_param ('notification', $notif);
Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array ('id' => $id)), true);
}
Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' — ' . $this->view->flux->name () . ' · ');
}
} else {
Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' · ');
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
}
public function displayAction () {
/**
* This action handles the display configuration page.
*
* It displays the display configuration page.
* If this action is reached through a POST request, it stores all new
* configuration values then sends a notification to the user.
*
* The options available on the page are:
* - language (default: en)
* - theme (default: Origin)
* - content width (default: thin)
* - display of read action in header
* - display of favorite action in header
* - display of date in header
* - display of open action in header
* - display of read action in footer
* - display of favorite action in footer
* - display of sharing action in footer
* - display of tags in footer
* - display of date in footer
* - display of open action in footer
* - html5 notification timeout (default: 0)
* Default values are false unless specified.
*/
public function displayAction() {
if (Minz_Request::isPost()) {
$this->view->conf->_language(Minz_Request::param('language', 'en'));
$this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10));
$this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal'));
$this->view->conf->_default_view (Minz_Request::param('default_view', 'a'));
$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->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
$this->view->conf->_lazyload (Minz_Request::param('lazyload', false));
$this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC'));
$this->view->conf->_mark_when (array(
'article' => Minz_Request::param('mark_open_article', false),
'site' => Minz_Request::param('mark_open_site', false),
'scroll' => Minz_Request::param('mark_scroll', false),
'reception' => Minz_Request::param('mark_upon_reception', false),
));
$themeId = Minz_Request::param('theme', '');
if ($themeId == '') {
$themeId = FreshRSS_Themes::defaultTheme;
}
$this->view->conf->_theme($themeId);
$this->view->conf->_topline_read(Minz_Request::param('topline_read', false));
$this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false));
$this->view->conf->_topline_date(Minz_Request::param('topline_date', false));
$this->view->conf->_topline_link(Minz_Request::param('topline_link', false));
$this->view->conf->_bottomline_read(Minz_Request::param('bottomline_read', false));
$this->view->conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false));
$this->view->conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false));
$this->view->conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false));
$this->view->conf->_bottomline_date(Minz_Request::param('bottomline_date', false));
$this->view->conf->_bottomline_link(Minz_Request::param('bottomline_link', false));
$this->view->conf->save();
FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en');
FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme);
FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin');
FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false);
FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false);
FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false);
FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false);
FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false);
FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::param('bottomline_sharing', false);
FreshRSS_Context::$user_conf->bottomline_tags = Minz_Request::param('bottomline_tags', false);
FreshRSS_Context::$user_conf->bottomline_date = Minz_Request::param('bottomline_date', false);
FreshRSS_Context::$user_conf->bottomline_link = Minz_Request::param('bottomline_link', false);
FreshRSS_Context::$user_conf->html5_notif_timeout = Minz_Request::param('html5_notif_timeout', 0);
FreshRSS_Context::$user_conf->save();
Minz_Session::_param ('language', $this->view->conf->language);
Minz_Translate::reset ();
Minz_Session::_param('language', FreshRSS_Context::$user_conf->language);
Minz_Translate::reset(FreshRSS_Context::$user_conf->language);
invalidateHttpCache();
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('configuration_updated')
);
Minz_Session::_param ('notification', $notif);
Minz_Request::forward (array ('c' => 'configure', 'a' => 'display'), true);
Minz_Request::good(_t('feedback.conf.updated'),
array('c' => 'configure', 'a' => 'display'));
}
$this->view->themes = FreshRSS_Themes::get();
Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · ');
Minz_View::prependTitle(_t('conf.display.title') . ' · ');
}
public function sharingAction () {
if (Minz_Request::isPost ()) {
$this->view->conf->_sharing (array(
'shaarli' => Minz_Request::param ('shaarli', false),
'wallabag' => Minz_Request::param ('wallabag', false),
'diaspora' => Minz_Request::param ('diaspora', false),
'twitter' => Minz_Request::param ('twitter', false),
'g+' => Minz_Request::param ('g+', false),
'facebook' => Minz_Request::param ('facebook', false),
'email' => Minz_Request::param ('email', false),
'print' => Minz_Request::param ('print', false),
));
$this->view->conf->save();
/**
* This action handles the reading configuration page.
*
* It displays the reading configuration page.
* If this action is reached through a POST request, it stores all new
* configuration values then sends a notification to the user.
*
* The options available on the page are:
* - number of posts per page (default: 10)
* - view mode (default: normal)
* - default article view (default: all)
* - load automatically articles
* - display expanded articles
* - display expanded categories
* - hide categories and feeds without unread articles
* - jump on next category or feed when marked as read
* - image lazy loading
* - stick open articles to the top
* - display a confirmation when reading all articles
* - auto remove article after reading
* - article order (default: DESC)
* - mark articles as read when:
* - displayed
* - opened on site
* - scrolled
* - received
* Default values are false unless specified.
*/
public function readingAction() {
if (Minz_Request::isPost()) {
FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10);
FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal');
FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive');
FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false);
FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false);
FreshRSS_Context::$user_conf->display_categories = Minz_Request::param('display_categories', false);
FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false);
FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false);
FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false);
FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false);
FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false);
FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false);
FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', false);
FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC');
FreshRSS_Context::$user_conf->mark_when = array(
'article' => Minz_Request::param('mark_open_article', false),
'site' => Minz_Request::param('mark_open_site', false),
'scroll' => Minz_Request::param('mark_scroll', false),
'reception' => Minz_Request::param('mark_upon_reception', false),
);
FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('configuration_updated')
);
Minz_Session::_param ('notification', $notif);
Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true);
Minz_Request::good(_t('feedback.conf.updated'),
array('c' => 'configure', 'a' => 'reading'));
}
Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · ');
Minz_View::prependTitle(_t('conf.reading.title') . ' · ');
}
public function importExportAction () {
require_once(LIB_PATH . '/lib_opml.php');
$catDAO = new FreshRSS_CategoryDAO ();
$this->view->categories = $catDAO->listCategories ();
/**
* This action handles the sharing configuration page.
*
* It displays the sharing configuration page.
* If this action is reached through a POST request, it stores all
* configuration values then sends a notification to the user.
*/
public function sharingAction() {
if (Minz_Request::isPost()) {
$params = Minz_Request::fetchPOST();
FreshRSS_Context::$user_conf->sharing = $params['share'];
FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
$this->view->req = Minz_Request::param ('q');
if ($this->view->req == 'export') {
Minz_View::_title ('freshrss_feeds.opml');
$this->view->_useLayout (false);
header('Content-Type: application/xml; charset=utf-8');
header('Content-disposition: attachment; filename=freshrss_feeds.opml');
$feedDAO = new FreshRSS_FeedDAO ();
$catDAO = new FreshRSS_CategoryDAO ();
$list = array ();
foreach ($catDAO->listCategories () as $key => $cat) {
$list[$key]['name'] = $cat->name ();
$list[$key]['feeds'] = $feedDAO->listByCategory ($cat->id ());
}
$this->view->categories = $list;
} elseif ($this->view->req == 'import' && Minz_Request::isPost ()) {
if ($_FILES['file']['error'] == 0) {
invalidateHttpCache();
// on parse le fichier OPML pour récupérer les catégories et les flux associés
try {
list ($categories, $feeds) = opml_import (
file_get_contents ($_FILES['file']['tmp_name'])
);
// On redirige vers le controller feed qui va se charger d'insérer les flux en BDD
// les flux sont mis au préalable dans des variables de Request
Minz_Request::_param ('q', 'null');
Minz_Request::_param ('categories', $categories);
Minz_Request::_param ('feeds', $feeds);
Minz_Request::forward (array ('c' => 'feed', 'a' => 'massiveImport'));
} catch (FreshRSS_Opml_Exception $e) {
Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('bad_opml_file')
);
Minz_Session::_param ('notification', $notif);
Minz_Request::forward (array (
'c' => 'configure',
'a' => 'importExport'
), true);
}
}
Minz_Request::good(_t('feedback.conf.updated'),
array('c' => 'configure', 'a' => 'sharing'));
}
$feedDAO = new FreshRSS_FeedDAO ();
$this->view->feeds = $feedDAO->listFeeds ();
// au niveau de la vue, permet de ne pas voir un flux sélectionné dans la liste
$this->view->flux = false;
Minz_View::prependTitle (Minz_Translate::t ('import_export_opml') . ' · ');
Minz_View::prependTitle(_t('conf.sharing.title') . ' · ');
}
public function shortcutAction () {
$list_keys = array ('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left',
'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right',
's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y',
'z', '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9',
'f10', 'f11', 'f12');
/**
* This action handles the shortcut configuration page.
*
* It displays the shortcut configuration page.
* If this action is reached through a POST request, it stores all new
* configuration values then sends a notification to the user.
*
* The authorized values for shortcuts are letters (a to z), numbers (0
* to 9), function keys (f1 to f12), backspace, delete, down, end, enter,
* escape, home, insert, left, page down, page up, return, right, space,
* tab and up.
*/
public function shortcutAction() {
$list_keys = array('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left',
'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right',
's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y',
'z', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9',
'f10', 'f11', 'f12');
$this->view->list_keys = $list_keys;
if (Minz_Request::isPost ()) {
$shortcuts = Minz_Request::param ('shortcuts');
$shortcuts_ok = array ();
if (Minz_Request::isPost()) {
$shortcuts = Minz_Request::param('shortcuts');
$shortcuts_ok = array();
foreach ($shortcuts as $key => $value) {
if (in_array($value, $list_keys)) {
@@ -302,53 +182,150 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
}
}
$this->view->conf->_shortcuts ($shortcuts_ok);
$this->view->conf->save();
FreshRSS_Context::$user_conf->shortcuts = $shortcuts_ok;
FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('shortcuts_updated')
);
Minz_Session::_param ('notification', $notif);
Minz_Request::forward (array ('c' => 'configure', 'a' => 'shortcut'), true);
Minz_Request::good(_t('feedback.conf.shortcuts_updated'),
array('c' => 'configure', 'a' => 'shortcut'));
}
Minz_View::prependTitle (Minz_Translate::t ('shortcuts') . ' · ');
Minz_View::prependTitle(_t('conf.shortcut.title') . ' · ');
}
public function usersAction() {
Minz_View::prependTitle(Minz_Translate::t ('users') . ' · ');
}
public function archivingAction () {
/**
* This action handles the archive configuration page.
*
* It displays the archive configuration page.
* If this action is reached through a POST request, it stores all new
* configuration values then sends a notification to the user.
*
* The options available on that page are:
* - duration to retain old article (default: 3)
* - number of article to retain per feed (default: 0)
* - refresh frequency (default: -2)
*
* @todo explain why the default value is -2 but this value does not
* exist in the drop-down list
*/
public function archivingAction() {
if (Minz_Request::isPost()) {
$old = Minz_Request::param('old_entries', 3);
$keepHistoryDefault = Minz_Request::param('keep_history_default', 0);
$this->view->conf->_old_entries($old);
$this->view->conf->_keep_history_default($keepHistoryDefault);
$this->view->conf->save();
FreshRSS_Context::$user_conf->old_entries = Minz_Request::param('old_entries', 3);
FreshRSS_Context::$user_conf->keep_history_default = Minz_Request::param('keep_history_default', 0);
FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', -2);
FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
$notif = array(
'type' => 'good',
'content' => Minz_Translate::t('configuration_updated')
);
Minz_Session::_param('notification', $notif);
Minz_Request::forward(array('c' => 'configure', 'a' => 'archiving'), true);
Minz_Request::good(_t('feedback.conf.updated'),
array('c' => 'configure', 'a' => 'archiving'));
}
Minz_View::prependTitle(Minz_Translate::t('archiving_configuration') . ' · ');
Minz_View::prependTitle(_t('conf.archiving.title') . ' · ');
$entryDAO = new FreshRSS_EntryDAO();
$entryDAO = FreshRSS_Factory::createEntryDao();
$this->view->nb_total = $entryDAO->count();
$this->view->size_user = $entryDAO->size();
if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
if (FreshRSS_Auth::hasAccess('admin')) {
$this->view->size_total = $entryDAO->size(true);
}
}
/**
* This action handles the user queries configuration page.
*
* If this action is reached through a POST request, it stores all new
* configuration values then sends a notification to the user then
* redirect to the same page.
* If this action is not reached through a POST request, it displays the
* configuration page and verifies that every user query is runable by
* checking if categories and feeds are still in use.
*/
public function queriesAction() {
$category_dao = new FreshRSS_CategoryDAO();
$feed_dao = FreshRSS_Factory::createFeedDao();
if (Minz_Request::isPost()) {
$params = Minz_Request::param('queries', array());
foreach ($params as $key => $query) {
if (!$query['name']) {
$query['name'] = _t('conf.query.number', $key + 1);
}
$queries[] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao);
}
FreshRSS_Context::$user_conf->queries = $queries;
FreshRSS_Context::$user_conf->save();
Minz_Request::good(_t('feedback.conf.updated'),
array('c' => 'configure', 'a' => 'queries'));
} else {
$this->view->queries = array();
foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
$this->view->queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao);
}
}
Minz_View::prependTitle(_t('conf.query.title') . ' · ');
}
/**
* This action handles the creation of a user query.
*
* It gets the GET parameters and stores them in the configuration query
* storage. Before it is saved, the unwanted parameters are unset to keep
* lean data.
*/
public function addQueryAction() {
$category_dao = new FreshRSS_CategoryDAO();
$feed_dao = FreshRSS_Factory::createFeedDao();
$queries = array();
foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
$queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao);
}
$params = Minz_Request::fetchGET();
$params['url'] = Minz_Url::display(array('params' => $params));
$params['name'] = _t('conf.query.number', count($queries) + 1);
$queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao);
FreshRSS_Context::$user_conf->queries = $queries;
FreshRSS_Context::$user_conf->save();
Minz_Request::good(_t('feedback.conf.query_created', $query['name']),
array('c' => 'configure', 'a' => 'queries'));
}
/**
* This action handles the system configuration page.
*
* It displays the system configuration page.
* If this action is reach through a POST request, it stores all new
* configuration values then sends a notification to the user.
*
* The options available on the page are:
* - user limit (default: 1)
* - user category limit (default: 16384)
* - user feed limit (default: 16384)
*/
public function systemAction() {
if (!FreshRSS_Auth::hasAccess('admin')) {
Minz_Error::error(403);
}
if (Minz_Request::isPost()) {
$limits = FreshRSS_Context::$system_conf->limits;
$limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
$limits['max_feeds'] = Minz_Request::param('max-feeds', 16384);
$limits['max_categories'] = Minz_Request::param('max-categories', 16384);
FreshRSS_Context::$system_conf->limits = $limits;
FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS');
FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false);
FreshRSS_Context::$system_conf->save();
invalidateHttpCache();
Minz_Session::_param('notification', array(
'type' => 'good',
'content' => _t('feedback.conf.updated')
));
}
}
}

View File

@@ -1,158 +1,203 @@
<?php
/**
* Controller to handle every entry actions.
*/
class FreshRSS_entry_Controller extends Minz_ActionController {
public function firstAction () {
if (!$this->view->loginOk) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
$this->params = array ();
$output = Minz_Request::param('output', '');
if (($output != '') && ($this->view->conf->view_mode !== $output)) {
$this->params['output'] = $output;
}
$this->redirect = false;
$ajax = Minz_Request::param ('ajax');
if ($ajax) {
$this->view->_useLayout (false);
// If ajax request, we do not print layout
$this->ajax = Minz_Request::param('ajax');
if ($this->ajax) {
$this->view->_useLayout(false);
Minz_Request::_param('ajax');
}
}
public function lastAction () {
$ajax = Minz_Request::param ('ajax');
if (!$ajax && $this->redirect) {
Minz_Request::forward (array (
'c' => 'index',
'a' => 'index',
'params' => $this->params
), true);
/**
* Mark one or several entries as read (or not!).
*
* If request concerns several entries, it MUST be a POST request.
* If request concerns several entries, only mark them as read is available.
*
* Parameters are:
* - id (default: false)
* - get (default: false) /(c_\d+|f_\d+|s|a)/
* - nextGet (default: $get)
* - idMax (default: 0)
* - is_read (default: true)
*/
public function readAction() {
$id = Minz_Request::param('id');
$get = Minz_Request::param('get');
$next_get = Minz_Request::param('nextGet', $get);
$id_max = Minz_Request::param('idMax', 0);
FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
FreshRSS_Context::$state = Minz_Request::param('state', 0);
if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) {
FreshRSS_Context::$state = FreshRSS_Entry::STATE_FAVORITE;
} elseif (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) {
FreshRSS_Context::$state = FreshRSS_Entry::STATE_NOT_FAVORITE;
} else {
Minz_Request::_param ('ajax');
FreshRSS_Context::$state = 0;
}
}
public function readAction () {
$this->redirect = true;
$params = array();
$id = Minz_Request::param ('id');
$get = Minz_Request::param ('get');
$nextGet = Minz_Request::param ('nextGet', $get);
$idMax = Minz_Request::param ('idMax', 0);
$entryDAO = new FreshRSS_EntryDAO ();
if ($id == false) {
if (!$get) {
$entryDAO->markReadEntries ($idMax);
} else {
$typeGet = $get[0];
$get = substr ($get, 2);
switch ($typeGet) {
case 'c':
$entryDAO->markReadCat ($get, $idMax);
break;
case 'f':
$entryDAO->markReadFeed ($get, $idMax);
break;
case 's':
$entryDAO->markReadEntries ($idMax, true);
break;
case 'a':
$entryDAO->markReadEntries ($idMax);
break;
}
if ($nextGet !== 'a') {
$this->params['get'] = $nextGet;
}
$entryDAO = FreshRSS_Factory::createEntryDao();
if ($id === false) {
// id is false? It MUST be a POST request!
if (!Minz_Request::isPost()) {
Minz_Request::bad(_t('feedback.access.not_found'), array('c' => 'index', 'a' => 'index'));
return;
}
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feeds_marked_read')
);
Minz_Session::_param ('notification', $notif);
if (!$get) {
// No get? Mark all entries as read (from $id_max)
$entryDAO->markReadEntries($id_max);
} else {
$type_get = $get[0];
$get = substr($get, 2);
switch($type_get) {
case 'c':
$entryDAO->markReadCat($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state);
break;
case 'f':
$entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state);
break;
case 's':
$entryDAO->markReadEntries($id_max, true, 0, FreshRSS_Context::$search);
break;
case 'a':
$entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state);
break;
}
if ($next_get !== 'a') {
// Redirect to the correct page (category, feed or starred)
// Not "a" because it is the default value if nothing is
// given.
$params['get'] = $next_get;
}
}
} else {
$is_read = (bool)(Minz_Request::param ('is_read', true));
$entryDAO->markRead ($id, $is_read);
$is_read = (bool)(Minz_Request::param('is_read', true));
$entryDAO->markRead($id, $is_read);
}
if (!$this->ajax) {
Minz_Request::good(_t('feedback.sub.feed.marked_read'), array(
'c' => 'index',
'a' => 'index',
'params' => $params,
), true);
}
}
public function bookmarkAction () {
$this->redirect = true;
/**
* This action marks an entry as favourite (bookmark) or not.
*
* Parameter is:
* - id (default: false)
* - is_favorite (default: true)
* If id is false, nothing happened.
*/
public function bookmarkAction() {
$id = Minz_Request::param('id');
$is_favourite = (bool)Minz_Request::param('is_favorite', true);
if ($id !== false) {
$entryDAO = FreshRSS_Factory::createEntryDao();
$entryDAO->markFavorite($id, $is_favourite);
}
$id = Minz_Request::param ('id');
if ($id) {
$entryDAO = new FreshRSS_EntryDAO ();
$entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true)));
if (!$this->ajax) {
Minz_Request::forward(array(
'c' => 'index',
'a' => 'index',
), true);
}
}
/**
* This action optimizes database to reduce its size.
*
* This action shouldbe reached by a POST request.
*
* @todo move this action in configure controller.
* @todo call this action through web-cron when available
*/
public function optimizeAction() {
if (Minz_Request::isPost()) {
@set_time_limit(300);
$url_redirect = array(
'c' => 'configure',
'a' => 'archiving',
);
// La table des entrées a tendance à grossir énormément
// Cette action permet d'optimiser cette table permettant de grapiller un peu de place
// Cette fonctionnalité n'est à appeler qu'occasionnellement
$entryDAO = new FreshRSS_EntryDAO();
$entryDAO->optimizeTable();
invalidateHttpCache();
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('optimization_complete')
);
Minz_Session::_param ('notification', $notif);
if (!Minz_Request::isPost()) {
Minz_Request::forward($url_redirect, true);
}
Minz_Request::forward(array(
'c' => 'configure',
'a' => 'archiving'
), true);
@set_time_limit(300);
$entryDAO = FreshRSS_Factory::createEntryDao();
$entryDAO->optimizeTable();
$feedDAO = FreshRSS_Factory::createFeedDao();
$feedDAO->updateCachedValues();
invalidateHttpCache();
Minz_Request::good(_t('feedback.admin.optimization_complete'), $url_redirect);
}
/**
* This action purges old entries from feeds.
*
* @todo should be a POST request
* @todo should be in feedController
*/
public function purgeAction() {
@set_time_limit(300);
$nb_month_old = max($this->view->conf->old_entries, 1);
$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
$feedDAO = new FreshRSS_FeedDAO();
$feeds = $feedDAO->listFeedsOrderUpdate();
$nbTotal = 0;
$feedDAO = FreshRSS_Factory::createFeedDao();
$feeds = $feedDAO->listFeeds();
$nb_total = 0;
invalidateHttpCache();
foreach ($feeds as $feed) {
$feedHistory = $feed->keepHistory();
if ($feedHistory == -2) { //default
$feedHistory = $this->view->conf->keep_history_default;
$feed_history = $feed->keepHistory();
if ($feed_history == -2) {
// TODO: -2 must be a constant!
// -2 means we take the default value from configuration
$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
}
if ($feedHistory >= 0) {
$nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feedHistory);
if ($feed_history >= 0) {
$nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feed_history);
if ($nb > 0) {
$nbTotal += $nb;
Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG);
$feedDAO->updateLastUpdate($feed->id());
$nb_total += $nb;
Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']');
}
}
}
$feedDAO->updateCachedValues();
invalidateHttpCache();
$notif = array(
'type' => 'good',
'content' => Minz_Translate::t('purge_completed', $nbTotal)
);
Minz_Session::_param('notification', $notif);
Minz_Request::forward(array(
Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array(
'c' => 'configure',
'a' => 'archiving'
), true);
));
}
}

View File

@@ -1,26 +1,53 @@
<?php
/**
* Controller to handle error page.
*/
class FreshRSS_error_Controller extends Minz_ActionController {
public function indexAction () {
switch (Minz_Request::param ('code')) {
case 403:
$this->view->code = 'Error 403 - Forbidden';
/**
* This action is the default one for the controller.
*
* It is called by Minz_Error::error() method.
*
* Parameters are passed by Minz_Session to have a proper url:
* - error_code (default: 404)
* - error_logs (default: array())
*/
public function indexAction() {
$code_int = Minz_Session::param('error_code', 404);
$error_logs = Minz_Session::param('error_logs', array());
Minz_Session::_param('error_code');
Minz_Session::_param('error_logs');
switch ($code_int) {
case 200 :
header('HTTP/1.1 200 OK');
break;
case 404:
$this->view->code = 'Error 404 - Not found';
case 403:
header('HTTP/1.1 403 Forbidden');
$this->view->code = 'Error 403 - Forbidden';
$this->view->errorMessage = _t('feedback.access.denied');
break;
case 500:
header('HTTP/1.1 500 Internal Server Error');
$this->view->code = 'Error 500 - Internal Server Error';
break;
case 503:
header('HTTP/1.1 503 Service Unavailable');
$this->view->code = 'Error 503 - Service Unavailable';
break;
case 404:
default:
header('HTTP/1.1 404 Not Found');
$this->view->code = 'Error 404 - Not found';
$this->view->errorMessage = _t('feedback.access.not_found');
}
$this->view->logs = Minz_Request::param ('logs');
Minz_View::prependTitle ($this->view->code . ' · ');
$error_message = trim(implode($error_logs));
if ($error_message !== '') {
$this->view->errorMessage = $error_message;
}
Minz_View::prependTitle($this->view->code . ' · ');
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* The controller to manage extensions.
*/
class FreshRSS_extension_Controller extends Minz_ActionController {
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
}
/**
* This action lists all the extensions available to the current user.
*/
public function indexAction() {
Minz_View::prependTitle(_t('admin.extensions.title') . ' · ');
$this->view->extension_list = array(
'system' => array(),
'user' => array(),
);
$extensions = Minz_ExtensionManager::listExtensions();
foreach ($extensions as $ext) {
$this->view->extension_list[$ext->getType()][] = $ext;
}
}
/**
* This action handles configuration of a given extension.
*
* Only administrator can configure a system extension.
*
* Parameters are:
* - e: the extension name (urlencoded)
* - additional parameters which should be handle by the extension
* handleConfigureAction() method (POST request).
*/
public function configureAction() {
if (Minz_Request::param('ajax')) {
$this->view->_useLayout(false);
} else {
$this->indexAction();
$this->view->change_view('extension', 'index');
}
$ext_name = urldecode(Minz_Request::param('e'));
$ext = Minz_ExtensionManager::findExtension($ext_name);
if (is_null($ext)) {
Minz_Error::error(404);
}
if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) {
Minz_Error::error(403);
}
$this->view->extension = $ext;
$this->view->extension->handleConfigureAction();
}
/**
* This action enables a disabled extension for the current user.
*
* System extensions can only be enabled by an administrator.
* This action must be reached by a POST request.
*
* Parameter is:
* - e: the extension name (urlencoded).
*/
public function enableAction() {
$url_redirect = array('c' => 'extension', 'a' => 'index');
if (Minz_Request::isPost()) {
$ext_name = urldecode(Minz_Request::param('e'));
$ext = Minz_ExtensionManager::findExtension($ext_name);
if (is_null($ext)) {
Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name),
$url_redirect);
}
if ($ext->isEnabled()) {
Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name),
$url_redirect);
}
$conf = null;
if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) {
$conf = FreshRSS_Context::$system_conf;
} elseif ($ext->getType() === 'user') {
$conf = FreshRSS_Context::$user_conf;
} else {
Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name),
$url_redirect);
}
$res = $ext->install();
if ($res === true) {
$ext_list = $conf->extensions_enabled;
array_push_unique($ext_list, $ext_name);
$conf->extensions_enabled = $ext_list;
$conf->save();
Minz_Request::good(_t('feedback.extensions.enable.ok', $ext_name),
$url_redirect);
} else {
Minz_Log::warning('Can not enable extension ' . $ext_name . ': ' . $res);
Minz_Request::bad(_t('feedback.extensions.enable.ko', $ext_name, _url('index', 'logs')),
$url_redirect);
}
}
Minz_Request::forward($url_redirect, true);
}
/**
* This action disables an enabled extension for the current user.
*
* System extensions can only be disabled by an administrator.
* This action must be reached by a POST request.
*
* Parameter is:
* - e: the extension name (urlencoded).
*/
public function disableAction() {
$url_redirect = array('c' => 'extension', 'a' => 'index');
if (Minz_Request::isPost()) {
$ext_name = urldecode(Minz_Request::param('e'));
$ext = Minz_ExtensionManager::findExtension($ext_name);
if (is_null($ext)) {
Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name),
$url_redirect);
}
if (!$ext->isEnabled()) {
Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name),
$url_redirect);
}
$conf = null;
if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) {
$conf = FreshRSS_Context::$system_conf;
} elseif ($ext->getType() === 'user') {
$conf = FreshRSS_Context::$user_conf;
} else {
Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name),
$url_redirect);
}
$res = $ext->uninstall();
if ($res === true) {
$ext_list = $conf->extensions_enabled;
array_remove($ext_list, $ext_name);
$conf->extensions_enabled = $ext_list;
$conf->save();
Minz_Request::good(_t('feedback.extensions.disable.ok', $ext_name),
$url_redirect);
} else {
Minz_Log::warning('Can not unable extension ' . $ext_name . ': ' . $res);
Minz_Request::bad(_t('feedback.extensions.disable.ko', $ext_name, _url('index', 'logs')),
$url_redirect);
}
}
Minz_Request::forward($url_redirect, true);
}
/**
* This action handles deletion of an extension.
*
* Only administrator can remove an extension.
* This action must be reached by a POST request.
*
* Parameter is:
* -e: extension name (urlencoded)
*/
public function removeAction() {
if (!FreshRSS_Auth::hasAccess('admin')) {
Minz_Error::error(403);
}
$url_redirect = array('c' => 'extension', 'a' => 'index');
if (Minz_Request::isPost()) {
$ext_name = urldecode(Minz_Request::param('e'));
$ext = Minz_ExtensionManager::findExtension($ext_name);
if (is_null($ext)) {
Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name),
$url_redirect);
}
$res = recursive_unlink($ext->getPath());
if ($res) {
Minz_Request::good(_t('feedback.extensions.removed', $ext_name),
$url_redirect);
} else {
Minz_Request::bad(_t('feedback.extensions.cannot_delete', $ext_name),
$url_redirect);
}
}
Minz_Request::forward($url_redirect, true);
}
}

View File

@@ -1,451 +1,591 @@
<?php
/**
* Controller to handle every feed actions.
*/
class FreshRSS_feed_Controller extends Minz_ActionController {
public function firstAction () {
if (!$this->view->loginOk) {
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
// Token is useful in the case that anonymous refresh is forbidden
// and CRON task cannot be used with php command so the user can
// set a CRON task to refresh his feeds by using token inside url
$token = $this->view->conf->token;
$token_param = Minz_Request::param ('token', '');
$token = FreshRSS_Context::$user_conf->token;
$token_param = Minz_Request::param('token', '');
$token_is_ok = ($token != '' && $token == $token_param);
$action = Minz_Request::actionName ();
if (!(($token_is_ok || Minz_Configuration::allowAnonymousRefresh()) &&
$action === 'actualize')
) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
$action = Minz_Request::actionName();
$allow_anonymous_refresh = FreshRSS_Context::$system_conf->allow_anonymous_refresh;
if ($action !== 'actualize' ||
!($allow_anonymous_refresh || $token_is_ok)) {
Minz_Error::error(403);
}
}
}
public function addAction () {
public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') {
FreshRSS_UserDAO::touch();
@set_time_limit(300);
if (Minz_Request::isPost ()) {
$this->catDAO = new FreshRSS_CategoryDAO ();
$this->catDAO->checkDefault ();
$catDAO = new FreshRSS_CategoryDAO();
$url = Minz_Request::param ('url_rss');
$cat = Minz_Request::param ('category', false);
if ($cat === false) {
$def_cat = $this->catDAO->getDefault ();
$cat = $def_cat->id ();
$cat = null;
if ($cat_id > 0) {
$cat = $catDAO->searchById($cat_id);
}
if ($cat == null && $new_cat_name != '') {
$cat = $catDAO->addCategory(array('name' => $new_cat_name));
}
if ($cat == null) {
$catDAO->checkDefault();
}
$cat_id = $cat == null ? FreshRSS_CategoryDAO::defaultCategoryId : $cat->id();
$feed = new FreshRSS_Feed($url); //Throws FreshRSS_BadUrl_Exception
$feed->_httpAuth($http_auth);
$feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
$feed->_category($cat_id);
$feedDAO = FreshRSS_Factory::createFeedDao();
if ($feedDAO->searchByUrl($feed->url())) {
throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name());
}
// Call the extension hook
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
if ($feed === null) {
throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
}
$values = array(
'url' => $feed->url(),
'category' => $feed->category(),
'name' => $title != '' ? $title : $feed->name(),
'website' => $feed->website(),
'description' => $feed->description(),
'lastUpdate' => time(),
'httpAuth' => $feed->httpAuth(),
);
$id = $feedDAO->addFeed($values);
if (!$id) {
// There was an error in database... we cannot say what here.
throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
}
$feed->_id($id);
// Ok, feed has been added in database. Now we have to refresh entries.
self::actualizeFeed($id, $url, false, null, true);
return $feed;
}
/**
* This action subscribes to a feed.
*
* It can be reached by both GET and POST requests.
*
* GET request displays a form to add and configure a feed.
* Request parameter is:
* - url_rss (default: false)
*
* POST request adds a feed in database.
* Parameters are:
* - url_rss (default: false)
* - category (default: false)
* - new_category (required if category == 'nc')
* - http_user (default: false)
* - http_pass (default: false)
* It tries to get website information from RSS feed.
* If no category is given, feed is added to the default one.
*
* If url_rss is false, nothing happened.
*/
public function addAction() {
$url = Minz_Request::param('url_rss');
if ($url === false) {
// No url, do nothing
Minz_Request::forward(array(
'c' => 'subscription',
'a' => 'index'
), true);
}
$feedDAO = FreshRSS_Factory::createFeedDao();
$url_redirect = array(
'c' => 'subscription',
'a' => 'index',
'params' => array(),
);
$limits = FreshRSS_Context::$system_conf->limits;
$this->view->feeds = $feedDAO->listFeeds();
if (count($this->view->feeds) >= $limits['max_feeds']) {
Minz_Request::bad(_t('feedback.sub.feed.over_max', $limits['max_feeds']),
$url_redirect);
}
if (Minz_Request::isPost()) {
$cat = Minz_Request::param('category');
$new_cat_name = '';
if ($cat === 'nc') {
// User want to create a new category, new_category parameter
// must exist
$new_cat = Minz_Request::param('new_category');
$new_cat_name = isset($new_cat['name']) ? $new_cat['name'] : '';
}
$user = Minz_Request::param ('http_user');
$pass = Minz_Request::param ('http_pass');
$params = array ();
// HTTP information are useful if feed is protected behind a
// HTTP authentication
$user = trim(Minz_Request::param('http_user', ''));
$pass = Minz_Request::param('http_pass', '');
$http_auth = '';
if ($user != '' && $pass != '') { //TODO: Sanitize
$http_auth = $user . ':' . $pass;
}
$transactionStarted = false;
try {
$feed = new FreshRSS_Feed ($url);
$feed->_category ($cat);
$httpAuth = '';
if ($user != '' || $pass != '') {
$httpAuth = $user . ':' . $pass;
}
$feed->_httpAuth ($httpAuth);
$feed->load(true);
$feedDAO = new FreshRSS_FeedDAO ();
$values = array (
'url' => $feed->url (),
'category' => $feed->category (),
'name' => $feed->name (),
'website' => $feed->website (),
'description' => $feed->description (),
'lastUpdate' => time (),
'httpAuth' => $feed->httpAuth (),
);
if ($feedDAO->searchByUrl ($values['url'])) {
// on est déjà abonné à ce flux
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('already_subscribed', $feed->name ())
);
Minz_Session::_param ('notification', $notif);
} else {
$id = $feedDAO->addFeed ($values);
if (!$id) {
// problème au niveau de la base de données
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('feed_not_added', $feed->name ())
);
Minz_Session::_param ('notification', $notif);
} else {
$feed->_id ($id);
$feed->faviconPrepare();
$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
$entryDAO = new FreshRSS_EntryDAO ();
$entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order
// on calcule la date des articles les plus anciens qu'on accepte
$nb_month_old = $this->view->conf->old_entries;
$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
$transactionStarted = true;
$feedDAO->beginTransaction ();
// on ajoute les articles en masse sans vérification
foreach ($entries as $entry) {
$values = $entry->toArray ();
$values['id_feed'] = $feed->id ();
$values['id'] = min(time(), $entry->date (true)) . uSecString();
$values['is_read'] = $is_read;
$entryDAO->addEntry ($values);
}
$feedDAO->updateLastUpdate ($feed->id ());
$feedDAO->commit ();
$transactionStarted = false;
// ok, ajout terminé
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feed_added', $feed->name ())
);
Minz_Session::_param ('notification', $notif);
// permet de rediriger vers la page de conf du flux
$params['id'] = $feed->id ();
}
}
$feed = self::addFeed($url, '', $cat, $new_cat_name, $http_auth);
} catch (FreshRSS_BadUrl_Exception $e) {
Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('invalid_url', $url)
);
Minz_Session::_param ('notification', $notif);
// Given url was not a valid url!
Minz_Log::warning($e->getMessage());
Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect);
} catch (FreshRSS_Feed_Exception $e) {
Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('internal_problem_feed')
);
Minz_Session::_param ('notification', $notif);
// Something went bad (timeout, server not found, etc.)
Minz_Log::warning($e->getMessage());
Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
} catch (Minz_FileNotExistException $e) {
// Répertoire de cache n'existe pas
Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('internal_problem_feed')
);
Minz_Session::_param ('notification', $notif);
}
if ($transactionStarted) {
$feedDAO->rollBack ();
// Cache directory doesn't exist!
Minz_Log::error($e->getMessage());
Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
} catch (FreshRSS_AlreadySubscribed_Exception $e) {
Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect);
} catch (FreshRSS_FeedNotAdded_Exception $e) {
Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect);
}
Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
// Entries are in DB, we redirect to feed configuration page.
$url_redirect['params']['id'] = $feed->id();
Minz_Request::good(_t('feedback.sub.feed.added', $feed->name()), $url_redirect);
} else {
// GET request: we must ask confirmation to user before adding feed.
Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
$this->catDAO = new FreshRSS_CategoryDAO();
$this->view->categories = $this->catDAO->listCategories(false);
$this->view->feed = new FreshRSS_Feed($url);
try {
// We try to get more information about the feed.
$this->view->feed->load(true);
$this->view->load_ok = true;
} catch (Exception $e) {
$this->view->load_ok = false;
}
$feed = $feedDAO->searchByUrl($this->view->feed->url());
if ($feed) {
// Already subscribe so we redirect to the feed configuration page.
$url_redirect['params']['id'] = $feed->id();
Minz_Request::good(_t('feedback.sub.feed.already_subscribed', $feed->name()), $url_redirect);
}
}
}
public function truncateAction () {
if (Minz_Request::isPost ()) {
$id = Minz_Request::param ('id');
$feedDAO = new FreshRSS_FeedDAO ();
$n = $feedDAO->truncate($id);
$notif = array(
'type' => $n === false ? 'bad' : 'good',
'content' => Minz_Translate::t ('n_entries_deleted', $n)
);
Minz_Session::_param ('notification', $notif);
invalidateHttpCache();
Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true);
/**
* This action remove entries from a given feed.
*
* It should be reached by a POST action.
*
* Parameter is:
* - id (default: false)
*/
public function truncateAction() {
$id = Minz_Request::param('id');
$url_redirect = array(
'c' => 'subscription',
'a' => 'index',
'params' => array('id' => $id)
);
if (!Minz_Request::isPost()) {
Minz_Request::forward($url_redirect, true);
}
$feedDAO = FreshRSS_Factory::createFeedDao();
$n = $feedDAO->truncate($id);
invalidateHttpCache();
if ($n === false) {
Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
} else {
Minz_Request::good(_t('feedback.sub.feed.n_entries_deleted', $n), $url_redirect);
}
}
public function actualizeAction () {
public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) {
@set_time_limit(300);
$feedDAO = new FreshRSS_FeedDAO ();
$entryDAO = new FreshRSS_EntryDAO ();
$feedDAO = FreshRSS_Factory::createFeedDao();
$entryDAO = FreshRSS_Factory::createEntryDao();
Minz_Session::_param('actualize_feeds', false);
$id = Minz_Request::param ('id');
$force = Minz_Request::param ('force', false);
// on créé la liste des flux à mettre à actualiser
// si on veut mettre un flux à jour spécifiquement, on le met
// dans la liste, mais seul (permet d'automatiser le traitement)
$feeds = array ();
if ($id) {
$feed = $feedDAO->searchById ($id);
// Create a list of feeds to actualize.
// If feed_id is set and valid, corresponding feed is added to the list but
// alone in order to automatize further process.
$feeds = array();
if ($feed_id > 0 || $feed_url) {
$feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url);
if ($feed) {
$feeds = array ($feed);
$feeds[] = $feed;
}
} else {
$feeds = $feedDAO->listFeedsOrderUpdate ();
$feeds = $feedDAO->listFeedsOrderUpdate(-1);
}
// on calcule la date des articles les plus anciens qu'on accepte
$nb_month_old = max($this->view->conf->old_entries, 1);
$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
// Calculate date of oldest entries we accept in DB.
$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
$i = 0;
$flux_update = 0;
$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
// PubSubHubbub support
$pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
$pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration.
$updated_feeds = 0;
$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
foreach ($feeds as $feed) {
$url = $feed->url(); //For detection of HTTP 301
$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
if ((!$simplePiePush) && (!$feed_id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
//$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
//Minz_Log::debug($text);
//file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
continue; //When PubSubHubbub is used, do not pull refresh so often
}
$mtime = 0;
$ttl = $feed->ttl();
if ($ttl == -1) {
continue; //Feed refresh is disabled
}
if ((!$simplePiePush) && (!$feed_id) &&
($feed->lastUpdate() + 10 >= time() - ($ttl == -2 ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) {
//Too early to refresh from source, but check whether the feed was updated by another user
$mtime = $feed->cacheModifiedTime();
if ($feed->lastUpdate() + 10 >= $mtime) {
continue; //Nothing newer from other users
}
//Minz_Log::debug($feed->url() . ' was updated at ' . date('c', $mtime) . ' by another user');
//Will take advantage of the newer cache
}
if (!$feed->lock()) {
Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE);
Minz_Log::notice('Feed already being actualized: ' . $feed->url());
continue;
}
try {
$url = $feed->url();
$feedHistory = $feed->keepHistory();
$feed->load(false);
$entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order
$hasTransaction = false;
if (count($entries) > 0) {
//For this feed, check last n entry GUIDs already in database
$existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1);
$useDeclaredDate = empty($existingGuids);
if ($feedHistory == -2) { //default
$feedHistory = $this->view->conf->keep_history_default;
}
$hasTransaction = true;
$feedDAO->beginTransaction();
// On ne vérifie pas strictement que l'article n'est pas déjà en BDD
// La BDD refusera l'ajout car (id_feed, guid) doit être unique
foreach ($entries as $entry) {
$eDate = $entry->date (true);
if ((!isset ($existingGuids[$entry->guid ()])) &&
(($feedHistory != 0) || ($eDate >= $date_min))) {
$values = $entry->toArray ();
//Use declared date at first import, otherwise use discovery date
$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
min(time(), $eDate) . uSecString() :
uTimeString();
$values['is_read'] = $is_read;
$entryDAO->addEntry ($values);
}
}
}
if (($feedHistory >= 0) && (rand(0, 30) === 1)) {
if (!$hasTransaction) {
$feedDAO->beginTransaction();
}
$nb = $feedDAO->cleanOldEntries ($feed->id (), $date_min, max($feedHistory, count($entries) + 10));
if ($nb > 0) {
Minz_Log::record ($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG);
}
}
// on indique que le flux vient d'être mis à jour en BDD
$feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction);
if ($hasTransaction) {
$feedDAO->commit();
}
$flux_update++;
if ($feed->url() !== $url) { //URL has changed (auto-discovery)
$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
if ($simplePiePush) {
$feed->loadEntries($simplePiePush); //Used by PubSubHubbub
} else {
$feed->load(false, $isNewFeed);
}
} catch (FreshRSS_Feed_Exception $e) {
Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
$feedDAO->updateLastUpdate ($feed->id (), 1);
Minz_Log::warning($e->getMessage());
$feedDAO->updateLastUpdate($feed->id(), true);
$feed->unlock();
continue;
}
$feed_history = $feed->keepHistory();
if ($isNewFeed) {
$feed_history = -1; //∞
} elseif ($feed_history == -2) {
// TODO: -2 must be a constant!
// -2 means we take the default value from configuration
$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
}
// We want chronological order and SimplePie uses reverse order.
$entries = array_reverse($feed->entries());
if (count($entries) > 0) {
$newGuids = array();
foreach ($entries as $entry) {
$newGuids[] = safe_ascii($entry->guid());
}
// For this feed, check existing GUIDs already in database.
$existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
unset($newGuids);
$oldGuids = array();
// Add entries in database if possible.
foreach ($entries as $entry) {
$entry_date = $entry->date(true);
if (isset($existingHashForGuids[$entry->guid()])) {
$existingHash = $existingHashForGuids[$entry->guid()];
if (strcasecmp($existingHash, $entry->hash()) === 0 || trim($existingHash, '0') == '') {
//This entry already exists and is unchanged. TODO: Remove the test with the zero'ed hash in FreshRSS v1.3
$oldGuids[] = $entry->guid();
} else { //This entry already exists but has been updated
//Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() .
//', old hash ' . $existingHash . ', new hash ' . $entry->hash());
//TODO: Make an updated/is_read policy by feed, in addition to the global one.
$entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null); //Change is_read according to policy.
if (!$entryDAO->inTransaction()) {
$entryDAO->beginTransaction();
}
$entryDAO->updateEntry($entry->toArray());
}
} elseif ($feed_history == 0 && $entry_date < $date_min) {
// This entry should not be added considering configuration and date.
$oldGuids[] = $entry->guid();
} else {
if ($isNewFeed) {
$id = min(time(), $entry_date) . uSecString();
} elseif ($entry_date < $date_min) {
$id = min(time(), $entry_date) . uSecString();
$entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read
} else {
$id = uTimeString();
$entry->_isRead($is_read);
}
$entry->_id($id);
$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
if ($entry === null) {
// An extension has returned a null value, there is nothing to insert.
continue;
}
if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull!
$text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . ' GUID ' . $entry->guid();
file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
Minz_Log::warning($text);
$pubSubHubbubEnabled = false;
$feed->pubSubHubbubError(true);
}
if (!$entryDAO->inTransaction()) {
$entryDAO->beginTransaction();
}
$entryDAO->addEntry($entry->toArray());
}
}
$entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime);
}
if ($feed_history >= 0 && rand(0, 30) === 1) {
// TODO: move this function in web cron when available (see entry::purge)
// Remove old entries once in 30.
if (!$entryDAO->inTransaction()) {
$entryDAO->beginTransaction();
}
$nb = $feedDAO->cleanOldEntries($feed->id(),
$date_min,
max($feed_history, count($entries) + 10));
if ($nb > 0) {
Minz_Log::debug($nb . ' old entries cleaned in feed [' .
$feed->url() . ']');
}
}
$feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime);
if ($entryDAO->inTransaction()) {
$entryDAO->commit();
}
if ($feed->hubUrl() && $feed->selfUrl()) { //selfUrl has priority for PubSubHubbub
if ($feed->selfUrl() !== $url) { //https://code.google.com/p/pubsubhubbub/wiki/MovingFeedsOrChangingHubs
$selfUrl = checkUrl($feed->selfUrl());
if ($selfUrl) {
Minz_Log::debug('PubSubHubbub unsubscribe ' . $feed->url());
if (!$feed->pubSubHubbubSubscribe(false)) { //Unsubscribe
Minz_Log::warning('Error while PubSubHubbub unsubscribing from ' . $feed->url());
}
$feed->_url($selfUrl, false);
Minz_Log::notice('Feed ' . $url . ' canonical address moved to ' . $feed->url());
$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
}
}
}
elseif ($feed->url() !== $url) { // HTTP 301 Moved Permanently
Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url());
$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
}
$feed->faviconPrepare();
if ($pubsubhubbubEnabledGeneral && $feed->pubSubHubbubPrepare()) {
Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url());
if (!$feed->pubSubHubbubSubscribe(true)) { //Subscribe
Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url());
}
}
$feed->unlock();
$updated_feeds++;
unset($feed);
// On arrête à 10 flux pour ne pas surcharger le serveur
// sauf si le paramètre $force est à vrai
$i++;
if ($i >= 10 && !$force) {
// No more than 10 feeds unless $force is true to avoid overloading
// the server.
if ($updated_feeds >= 10 && !$force) {
break;
}
}
$url = array ();
if ($flux_update === 1) {
// on a mis un seul flux à jour
$feed = reset ($feeds);
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feed_actualized', $feed->name ())
);
} elseif ($flux_update > 1) {
// plusieurs flux on été mis à jour
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('n_feeds_actualized', $flux_update)
);
} else {
// aucun flux n'a été mis à jour, oups
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('no_feed_to_refresh')
);
}
if ($i === 1) {
// Si on a voulu mettre à jour qu'un flux
// on filtre l'affichage par ce flux
$feed = reset ($feeds);
$url['params'] = array ('get' => 'f_' . $feed->id ());
}
if (Minz_Request::param ('ajax', 0) === 0) {
Minz_Session::_param ('notification', $notif);
Minz_Request::forward ($url, true);
} else {
// Une requête Ajax met un seul flux à jour.
// Comme en principe plusieurs requêtes ont lieu,
// on indique que "plusieurs flux ont été mis à jour".
// Cela permet d'avoir une notification plus proche du
// ressenti utilisateur
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feeds_actualized')
);
Minz_Session::_param ('notification', $notif);
// et on désactive le layout car ne sert à rien
$this->view->_useLayout (false);
}
return array($updated_feeds, reset($feeds));
}
public function massiveImportAction () {
@set_time_limit(300);
/**
* This action actualizes entries from one or several feeds.
*
* Parameters are:
* - id (default: false): Feed ID
* - url (default: false): Feed URL
* - force (default: false)
* If id and url are not specified, all the feeds are actualized. But if force is
* false, process stops at 10 feeds to avoid time execution problem.
*/
public function actualizeAction() {
Minz_Session::_param('actualize_feeds', false);
$id = Minz_Request::param('id');
$url = Minz_Request::param('url');
$force = Minz_Request::param('force');
$this->catDAO = new FreshRSS_CategoryDAO ();
$this->catDAO->checkDefault ();
list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force);
$entryDAO = new FreshRSS_EntryDAO ();
$feedDAO = new FreshRSS_FeedDAO ();
$categories = Minz_Request::param ('categories', array (), true);
$feeds = Minz_Request::param ('feeds', array (), true);
// on ajoute les catégories en masse dans une fonction à part
$this->addCategories ($categories);
// on calcule la date des articles les plus anciens qu'on accepte
$nb_month_old = $this->view->conf->old_entries;
$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
// la variable $error permet de savoir si une erreur est survenue
// Le but est de ne pas arrêter l'import même en cas d'erreur
// L'utilisateur sera mis au courant s'il y a eu des erreurs, mais
// ne connaîtra pas les détails. Ceux-ci seront toutefois logguées
$error = false;
$i = 0;
foreach ($feeds as $feed) {
try {
$values = array (
'id' => $feed->id (),
'url' => $feed->url (),
'category' => $feed->category (),
'name' => $feed->name (),
'website' => $feed->website (),
'description' => $feed->description (),
'lastUpdate' => 0,
'httpAuth' => $feed->httpAuth ()
);
// ajout du flux que s'il n'est pas déjà en BDD
if (!$feedDAO->searchByUrl ($values['url'])) {
$id = $feedDAO->addFeed ($values);
if ($id) {
$feed->_id ($id);
$feed->faviconPrepare();
} else {
$error = true;
}
}
} catch (FreshRSS_Feed_Exception $e) {
$error = true;
Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
}
}
if ($error) {
$res = Minz_Translate::t ('feeds_imported_with_errors');
if (Minz_Request::param('ajax')) {
// Most of the time, ajax request is for only one feed. But since
// there are several parallel requests, we should return that there
// are several updated feeds.
$notif = array(
'type' => 'good',
'content' => _t('feedback.sub.feed.actualizeds')
);
Minz_Session::_param('notification', $notif);
// No layout in ajax request.
$this->view->_useLayout(false);
} else {
$res = Minz_Translate::t ('feeds_imported');
}
$notif = array (
'type' => 'good',
'content' => $res
);
Minz_Session::_param ('notification', $notif);
Minz_Session::_param ('actualize_feeds', true);
// et on redirige vers la page d'accueil
Minz_Request::forward (array (
'c' => 'index',
'a' => 'index'
), true);
}
public function deleteAction () {
if (Minz_Request::isPost ()) {
$type = Minz_Request::param ('type', 'feed');
$id = Minz_Request::param ('id');
$feedDAO = new FreshRSS_FeedDAO ();
if ($type == 'category') {
if ($feedDAO->deleteFeedByCategory ($id)) {
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('category_emptied')
);
//TODO: Delete old favicons
} else {
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('error_occured')
);
}
// Redirect to the main page with correct notification.
if ($updated_feeds === 1) {
Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array(
'params' => array('get' => 'f_' . $feed->id())
));
} elseif ($updated_feeds > 1) {
Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array());
} else {
if ($feedDAO->deleteFeed ($id)) {
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feed_deleted')
);
//TODO: Delete old favicon
} else {
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('error_occured')
);
}
Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array());
}
}
return $updated_feeds;
}
Minz_Session::_param ('notification', $notif);
public static function renameFeed($feed_id, $feed_name) {
if ($feed_id <= 0 || $feed_name == '') {
return false;
}
FreshRSS_UserDAO::touch();
$feedDAO = FreshRSS_Factory::createFeedDao();
return $feedDAO->updateFeed($feed_id, array('name' => $feed_name));
}
if ($type == 'category') {
Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
} else {
Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed'), true);
}
public static function moveFeed($feed_id, $cat_id, $new_cat_name = '') {
if ($feed_id <= 0 || ($cat_id <= 0 && $new_cat_name == '')) {
return false;
}
FreshRSS_UserDAO::touch();
$catDAO = new FreshRSS_CategoryDAO();
if ($cat_id > 0) {
$cat = $catDAO->searchById($cat_id);
$cat_id = $cat == null ? 0 : $cat->id();
}
if ($cat_id <= 1 && $new_cat_name != '') {
$cat_id = $catDAO->addCategory(array('name' => $new_cat_name));
}
if ($cat_id <= 1) {
$catDAO->checkDefault();
$cat_id = FreshRSS_CategoryDAO::defaultCategoryId;
}
$feedDAO = FreshRSS_Factory::createFeedDao();
return $feedDAO->updateFeed($feed_id, array('category' => $cat_id));
}
/**
* This action changes the category of a feed.
*
* This page must be reached by a POST request.
*
* Parameters are:
* - f_id (default: false)
* - c_id (default: false)
* If c_id is false, default category is used.
*
* @todo should handle order of the feed inside the category.
*/
public function moveAction() {
if (!Minz_Request::isPost()) {
Minz_Request::forward(array('c' => 'subscription'), true);
}
$feed_id = Minz_Request::param('f_id');
$cat_id = Minz_Request::param('c_id');
if (self::moveFeed($feed_id, $cat_id)) {
// TODO: return something useful
} else {
Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
'in the category `' . $cat_id . '`');
Minz_Error::error(404);
}
}
private function addCategories ($categories) {
foreach ($categories as $cat) {
if (!$this->catDAO->searchByName ($cat->name ())) {
$values = array (
'id' => $cat->id (),
'name' => $cat->name (),
);
$catDAO->addCategory ($values);
}
public static function deleteFeed($feed_id) {
FreshRSS_UserDAO::touch();
$feedDAO = FreshRSS_Factory::createFeedDao();
if ($feedDAO->deleteFeed($feed_id)) {
// TODO: Delete old favicon
// Remove related queries
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
'f_' . $feed_id, FreshRSS_Context::$user_conf->queries);
FreshRSS_Context::$user_conf->save();
return true;
}
return false;
}
/**
* This action deletes a feed.
*
* This page must be reached by a POST request.
* If there are related queries, they are deleted too.
*
* Parameters are:
* - id (default: false)
* - r (default: false)
* r permits to redirect to a given page at the end of this action.
*
* @todo handle "r" redirection in Minz_Request::forward()?
*/
public function deleteAction() {
$redirect_url = Minz_Request::param('r', false, true);
if (!$redirect_url) {
$redirect_url = array('c' => 'subscription', 'a' => 'index');
}
if (!Minz_Request::isPost()) {
Minz_Request::forward($redirect_url, true);
}
$id = Minz_Request::param('id');
if (self::deleteFeed($id)) {
Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url);
} else {
Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url);
}
}
}

View File

@@ -0,0 +1,716 @@
<?php
/**
* Controller to handle every import and export actions.
*/
class FreshRSS_importExport_Controller extends Minz_ActionController {
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
require_once(LIB_PATH . '/lib_opml.php');
$this->catDAO = new FreshRSS_CategoryDAO();
$this->entryDAO = FreshRSS_Factory::createEntryDao();
$this->feedDAO = FreshRSS_Factory::createFeedDao();
}
/**
* This action displays the main page for import / export system.
*/
public function indexAction() {
$this->view->feeds = $this->feedDAO->listFeeds();
Minz_View::prependTitle(_t('sub.import_export.title') . ' · ');
}
public function importFile($name, $path, $username = null) {
require_once(LIB_PATH . '/lib_opml.php');
$this->catDAO = new FreshRSS_CategoryDAO($username);
$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
$type_file = self::guessFileType($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($path);
if (!is_resource($zip)) {
// zip_open cannot open file: something is wrong
throw new FreshRSS_Zip_Exception($zip);
}
while (($zipfile = zip_read($zip)) !== false) {
if (!is_resource($zipfile)) {
// zip_entry() can also return an error code!
throw new FreshRSS_Zip_Exception($zipfile);
} else {
$type_zipfile = self::guessFileType(zip_entry_name($zipfile));
if ($type_file !== 'unknown') {
$list_files[$type_zipfile][] = zip_entry_read(
$zipfile,
zip_entry_filesize($zipfile)
);
}
}
}
zip_close($zip);
} elseif ($type_file === 'zip') {
// ZIP extension is not loaded
throw new FreshRSS_ZipMissing_Exception();
} elseif ($type_file !== 'unknown') {
$list_files[$type_file][] = file_get_contents($path);
}
// Import file contents.
// OPML first(so categories and feeds are imported)
// Starred articles then so the "favourite" status is already set
// And finally all other files.
$ok = true;
foreach ($list_files['opml'] as $opml_file) {
if (!$this->importOpml($opml_file)) {
$ok = false;
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
} else {
Minz_Log::warning('Error during OPML import');
}
}
}
foreach ($list_files['json_starred'] as $article_file) {
if (!$this->importJson($article_file, true)) {
$ok = false;
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during JSON stars import' . "\n");
} else {
Minz_Log::warning('Error during JSON stars import');
}
}
}
foreach ($list_files['json_feed'] as $article_file) {
if (!$this->importJson($article_file)) {
$ok = false;
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during JSON feeds import' . "\n");
} else {
Minz_Log::warning('Error during JSON feeds import');
}
}
}
return $ok;
}
/**
* This action handles import action.
*
* It must be reached by a POST request.
*
* Parameter is:
* - file (default: nothing!)
* Available file types are: zip, json or xml.
*/
public function importAction() {
if (!Minz_Request::isPost()) {
Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
}
$file = $_FILES['file'];
$status_file = $file['error'];
if ($status_file !== 0) {
Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
array('c' => 'importExport', 'a' => 'index'));
}
@set_time_limit(300);
$error = false;
try {
$error = !$this->importFile($file['name'], $file['tmp_name']);
} catch (FreshRSS_ZipMissing_Exception $zme) {
Minz_Request::bad(_t('feedback.import_export.no_zip_extension'),
array('c' => 'importExport', 'a' => 'index'));
} catch (FreshRSS_Zip_Exception $ze) {
Minz_Log::warning('ZIP archive cannot be imported. Error code: ' . $ze->zipErrorCode());
Minz_Request::bad(_t('feedback.import_export.zip_error'),
array('c' => 'importExport', 'a' => 'index'));
}
// And finally, we get import status and redirect to the home page
Minz_Session::_param('actualize_feeds', true);
$content_notif = $error === true ? _t('feedback.import_export.feeds_imported_with_errors') :
_t('feedback.import_export.feeds_imported');
Minz_Request::good($content_notif);
}
/**
* This method tries to guess the file type based on its name.
*
* Itis a *very* basic guess file type function. Only based on filename.
* That's could be improved but should be enough for what we have to do.
*/
private static function guessFileType($filename) {
if (substr_compare($filename, '.zip', -4) === 0) {
return 'zip';
} elseif (substr_compare($filename, '.opml', -5) === 0 ||
substr_compare($filename, '.xml', -4) === 0) {
return 'opml';
} elseif (substr_compare($filename, '.json', -5) === 0 &&
strpos($filename, 'starred') !== false) {
return 'json_starred';
} elseif (substr_compare($filename, '.json', -5) === 0) {
return 'json_feed';
} else {
return 'unknown';
}
}
/**
* This method parses and imports an OPML file.
*
* @param string $opml_file the OPML file content.
* @return boolean false if an error occured, true otherwise.
*/
private function importOpml($opml_file) {
$opml_array = array();
try {
$opml_array = libopml_parse_string($opml_file, false);
} catch (LibOPML_Exception $e) {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
} else {
Minz_Log::warning($e->getMessage());
}
return false;
}
$this->catDAO->checkDefault();
return $this->addOpmlElements($opml_array['body']);
}
/**
* This method imports an OPML file based on its body.
*
* @param array $opml_elements an OPML element (body or outline).
* @param string $parent_cat the name of the parent category.
* @return boolean false if an error occured, true otherwise.
*/
private function addOpmlElements($opml_elements, $parent_cat = null) {
$ok = true;
$nb_feeds = count($this->feedDAO->listFeeds());
$nb_cats = count($this->catDAO->listCategories(false));
$limits = FreshRSS_Context::$system_conf->limits;
foreach ($opml_elements as $elt) {
if (isset($elt['xmlUrl'])) {
// If xmlUrl exists, it means it is a feed
if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
Minz_Log::warning(_t('feedback.sub.feed.over_max',
$limits['max_feeds']));
$ok = false;
continue;
}
if ($this->addFeedOpml($elt, $parent_cat)) {
$nb_feeds++;
} else {
$ok = false;
}
} else {
// No xmlUrl? It should be a category!
$limit_reached = ($nb_cats >= $limits['max_categories']);
if (!FreshRSS_Context::$isCli && $limit_reached) {
Minz_Log::warning(_t('feedback.sub.category.over_max',
$limits['max_categories']));
$ok = false;
continue;
}
if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) {
$nb_cats++;
} else {
$ok = false;
}
}
}
return $ok;
}
/**
* This method imports an OPML feed element.
*
* @param array $feed_elt an OPML element (must be a feed element).
* @param string $parent_cat the name of the parent category.
* @return boolean false if an error occured, true otherwise.
*/
private function addFeedOpml($feed_elt, $parent_cat) {
if ($parent_cat == null) {
// This feed has no parent category so we get the default one
$this->catDAO->checkDefault();
$default_cat = $this->catDAO->getDefault();
$parent_cat = $default_cat->name();
}
$cat = $this->catDAO->searchByName($parent_cat);
if ($cat == null) {
// If there is not $cat, it means parent category does not exist in
// database.
// If it happens, take the default category.
$this->catDAO->checkDefault();
$cat = $this->catDAO->getDefault();
}
// We get different useful information
$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
$name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']);
$website = '';
if (isset($feed_elt['htmlUrl'])) {
$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']);
}
$description = '';
if (isset($feed_elt['description'])) {
$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
}
$error = false;
try {
// Create a Feed object and add it in DB
$feed = new FreshRSS_Feed($url);
$feed->_category($cat->id());
$feed->_name($name);
$feed->_website($website);
$feed->_description($description);
// Call the extension hook
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
if ($feed != null) {
// addFeedObject checks if feed is already in DB so nothing else to
// check here
$id = $this->feedDAO->addFeedObject($feed);
$error = ($id === false);
} else {
$error = true;
}
} catch (FreshRSS_Feed_Exception $e) {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
} else {
Minz_Log::warning($e->getMessage());
}
$error = true;
}
if ($error) {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n");
} else {
Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id());
}
}
return !$error;
}
/**
* This method imports an OPML category element.
*
* @param array $cat_elt an OPML element (must be a category element).
* @param string $parent_cat the name of the parent category.
* @param boolean $cat_limit_reached indicates if category limit has been reached.
* if yes, category is not added (but we try for feeds!)
* @return boolean false if an error occured, true otherwise.
*/
private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
// Create a new Category object
$catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
$cat = new FreshRSS_Category($catName);
$error = true;
if (FreshRSS_Context::$isCli || !$cat_limit_reached) {
$id = $this->catDAO->addCategoryObject($cat);
$error = ($id === false);
}
if ($error) {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
} else {
Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
}
}
if (isset($cat_elt['@outlines'])) {
// Our cat_elt contains more categories or more feeds, so we
// add them recursively.
// Note: FreshRSS does not support yet category arborescence
$error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName);
}
return !$error;
}
/**
* This method import a JSON-based file (Google Reader format).
*
* @param string $article_file the JSON file content.
* @param boolean $starred true if articles from the file must be starred.
* @return boolean false if an error occured, true otherwise.
*/
private function importJson($article_file, $starred = false) {
$article_object = json_decode($article_file, true);
if ($article_object == null) {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n");
} else {
Minz_Log::warning('Try to import a non-JSON file');
}
return false;
}
$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
$google_compliant = strpos($article_object['id'], 'com.google') !== false;
$error = false;
$article_to_feed = array();
$nb_feeds = count($this->feedDAO->listFeeds());
$limits = FreshRSS_Context::$system_conf->limits;
// First, we check feeds of articles are in DB (and add them if needed).
foreach ($article_object['items'] as $item) {
$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
$feed = new FreshRSS_Feed($item['origin'][$key]);
$feed = $this->feedDAO->searchByUrl($feed->url());
if ($feed == null) {
// Feed does not exist in DB,we should to try to add it.
if ((!FreshRSS_Context::$isCli) && ($nb_feeds >= $limits['max_feeds'])) {
// Oops, no more place!
Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds']));
} else {
$feed = $this->addFeedJson($item['origin'], $google_compliant);
}
if ($feed == null) {
// Still null? It means something went wrong.
$error = true;
} else {
$nb_feeds++;
}
}
if ($feed != null) {
$article_to_feed[$item['id']] = $feed->id();
}
}
$newGuids = array();
foreach ($article_object['items'] as $item) {
$newGuids[] = safe_ascii($item['id']);
}
// For this feed, check existing GUIDs already in database.
$existingHashForGuids = $this->entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
unset($newGuids);
// Then, articles are imported.
$this->entryDAO->beginTransaction();
foreach ($article_object['items'] as $item) {
if (!isset($article_to_feed[$item['id']])) {
// Related feed does not exist for this entry, do nothing.
continue;
}
$feed_id = $article_to_feed[$item['id']];
$author = isset($item['author']) ? $item['author'] : '';
$key_content = ($google_compliant && !isset($item['content'])) ?
'summary' : 'content';
$tags = $item['categories'];
if ($google_compliant) {
// Remove tags containing "/state/com.google" which are useless.
$tags = array_filter($tags, function($var) {
return strpos($var, '/state/com.google') !== false;
});
}
$entry = new FreshRSS_Entry(
$feed_id, $item['id'], $item['title'], $author,
$item[$key_content]['content'], $item['alternate'][0]['href'],
$item['published'], $is_read, $starred
);
$entry->_id(min(time(), $entry->date(true)) . uSecString());
$entry->_tags($tags);
$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
if ($entry == null) {
// An extension has returned a null value, there is nothing to insert.
continue;
}
$values = $entry->toArray();
if (isset($existingHashForGuids[$entry->guid()])) {
$id = $this->entryDAO->updateEntry($values);
} else {
$id = $this->entryDAO->addEntry($values);
}
if (!$error && ($id === false)) {
$error = true;
}
}
$this->entryDAO->commit();
return !$error;
}
/**
* This method import a JSON-based feed (Google Reader format).
*
* @param array $origin represents a feed.
* @param boolean $google_compliant takes care of some specific values if true.
* @return FreshRSS_Feed if feed is in database at the end of the process,
* else null.
*/
private function addFeedJson($origin, $google_compliant) {
$return = null;
$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
$url = $origin[$key];
$name = $origin['title'];
$website = $origin['htmlUrl'];
try {
// Create a Feed object and add it in database.
$feed = new FreshRSS_Feed($url);
$feed->_category(FreshRSS_CategoryDAO::defaultCategoryId);
$feed->_name($name);
$feed->_website($website);
// Call the extension hook
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
if ($feed != null) {
// addFeedObject checks if feed is already in DB so nothing else to
// check here.
$id = $this->feedDAO->addFeedObject($feed);
if ($id !== false) {
$feed->_id($id);
$return = $feed;
}
}
} catch (FreshRSS_Feed_Exception $e) {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error during JSON feed import: ' . $e->getMessage() . "\n");
} else {
Minz_Log::warning($e->getMessage());
}
}
return $return;
}
public function exportFile($export_opml = true, $export_starred = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) {
require_once(LIB_PATH . '/lib_opml.php');
$this->catDAO = new FreshRSS_CategoryDAO($username);
$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
$this->entryDAO->disableBuffering();
if ($export_feeds === true) {
//All feeds
$export_feeds = $this->feedDAO->listFeedsIds();
}
if (!is_array($export_feeds)) {
$export_feeds = array();
}
$day = date('Y-m-d');
$export_files = array();
if ($export_opml) {
$export_files["feeds_${day}.opml.xml"] = $this->generateOpml();
}
if ($export_starred) {
$export_files["starred_${day}.json"] = $this->generateEntries('starred');
}
foreach ($export_feeds as $feed_id) {
$feed = $this->feedDAO->searchById($feed_id);
if ($feed) {
$filename = "feed_${day}_" . $feed->category() . '_'
. $feed->id() . '.json';
$export_files[$filename] = $this->generateEntries('feed', $feed, $maxFeedEntries);
}
}
$nb_files = count($export_files);
if ($nb_files > 1) {
// If there are more than 1 file to export, we need a ZIP archive.
try {
$this->sendZip($export_files);
} catch (Exception $e) {
throw new FreshRSS_ZipMissing_Exception($e);
}
} elseif ($nb_files === 1) {
// Only one file? Guess its type and export it.
$filename = key($export_files);
$type = self::guessFileType($filename);
$this->sendFile('freshrss_' . $filename, $export_files[$filename], $type);
}
return $nb_files;
}
/**
* This action handles export action.
*
* This action must be reached by a POST request.
*
* Parameters are:
* - export_opml (default: false)
* - export_starred (default: false)
* - export_feeds (default: array()) a list of feed ids
*/
public function exportAction() {
if (!Minz_Request::isPost()) {
Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
}
$this->view->_useLayout(false);
$nb_files = 0;
try {
$nb_files = $this->exportFile(
Minz_Request::param('export_opml', false),
Minz_Request::param('export_starred', false),
Minz_Request::param('export_feeds', array())
);
} catch (FreshRSS_ZipMissing_Exception $zme) {
# Oops, there is no ZIP extension!
Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'),
array('c' => 'importExport', 'a' => 'index'));
}
if ($nb_files < 1) {
// Nothing to do...
Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
}
}
/**
* This method returns the OPML file based on user subscriptions.
*
* @return string the OPML file content.
*/
private function generateOpml() {
$list = array();
foreach ($this->catDAO->listCategories() as $key => $cat) {
$list[$key]['name'] = $cat->name();
$list[$key]['feeds'] = $this->feedDAO->listByCategory($cat->id());
}
$this->view->categories = $list;
return $this->view->helperToString('export/opml');
}
/**
* This method returns a JSON file content.
*
* @param string $type must be "starred" or "feed"
* @param FreshRSS_Feed $feed feed of which we want to get entries.
* @return string the JSON file content.
*/
private function generateEntries($type, $feed = NULL, $maxFeedEntries = 50) {
$this->view->categories = $this->catDAO->listCategories();
if ($type == 'starred') {
$this->view->list_title = _t('sub.import_export.starred_list');
$this->view->type = 'starred';
$unread_fav = $this->entryDAO->countUnreadReadFavorites();
$this->view->entriesRaw = $this->entryDAO->listWhereRaw(
's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all']
);
} elseif ($type === 'feed' && $feed != null) {
$this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
$this->view->type = 'feed/' . $feed->id();
$this->view->entriesRaw = $this->entryDAO->listWhereRaw(
'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
$maxFeedEntries
);
$this->view->feed = $feed;
}
return $this->view->helperToString('export/articles');
}
/**
* This method zips a list of files and returns it by HTTP.
*
* @param array $files list of files where key is filename and value the content.
* @throws Exception if Zip extension is not loaded.
*/
private function sendZip($files) {
if (!extension_loaded('zip')) {
throw new Exception();
}
// From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
$zip_file = tempnam('tmp', 'zip');
$zip = new ZipArchive();
$zip->open($zip_file, ZipArchive::OVERWRITE);
foreach ($files as $filename => $content) {
$zip->addFromString($filename, $content);
}
// Close and send to user
$zip->close();
header('Content-Type: application/zip');
header('Content-Length: ' . filesize($zip_file));
$day = date('Y-m-d');
header('Content-Disposition: attachment; filename="freshrss_' . $day . '_export.zip"');
readfile($zip_file);
unlink($zip_file);
}
/**
* This method returns a single file (OPML or JSON) by HTTP.
*
* @param string $filename
* @param string $content
* @param string $type the file type (opml, json_feed or json_starred).
* If equals to unknown, nothing happens.
*/
private function sendFile($filename, $content, $type) {
if ($type === 'unknown') {
return;
}
$content_type = '';
if ($type === 'opml') {
$content_type = 'application/xml';
} elseif ($type === 'json_feed' || $type === 'json_starred') {
$content_type = 'application/json';
}
header('Content-Type: ' . $content_type . '; charset=utf-8');
header('Content-disposition: attachment; filename=' . $filename);
print($content);
}
}

View File

@@ -1,371 +1,272 @@
<?php
/**
* This class handles main actions of FreshRSS.
*/
class FreshRSS_index_Controller extends Minz_ActionController {
private $nb_not_read_cat = 0;
public function indexAction () {
$output = Minz_Request::param ('output');
$token = $this->view->conf->token;
// check if user is logged in
if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) {
$token_param = Minz_Request::param ('token', '');
$token_is_ok = ($token != '' && $token === $token_param);
if ($output === 'rss' && !$token_is_ok) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
return;
} elseif ($output !== 'rss') {
// "hard" redirection is not required, just ask dispatcher to
// forward to the login form without 302 redirection
Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'));
return;
}
}
// construction of RSS url of this feed
$params = Minz_Request::params ();
$params['output'] = 'rss';
if (isset ($params['search'])) {
$params['search'] = urlencode ($params['search']);
}
if (!Minz_Configuration::allowAnonymous()) {
$params['token'] = $token;
}
$this->view->rss_url = array (
/**
* This action only redirect on the default view mode (normal or global)
*/
public function indexAction() {
$prefered_output = FreshRSS_Context::$user_conf->view_mode;
Minz_Request::forward(array(
'c' => 'index',
'a' => 'index',
'params' => $params
);
'a' => $prefered_output
));
}
if ($output === 'rss') {
// no layout for RSS output
$this->view->_useLayout (false);
header('Content-Type: application/rss+xml; charset=utf-8');
} elseif ($output === 'global') {
Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
}
$catDAO = new FreshRSS_CategoryDAO();
$entryDAO = new FreshRSS_EntryDAO();
$this->view->cat_aside = $catDAO->listCategories ();
$this->view->nb_favorites = $entryDAO->countUnreadReadFavorites ();
$this->view->nb_not_read = FreshRSS_CategoryDAO::CountUnreads($this->view->cat_aside, 1);
$this->view->currentName = '';
$this->view->get_c = '';
$this->view->get_f = '';
$get = Minz_Request::param ('get', 'a');
$getType = $get[0];
$getId = substr ($get, 2);
if (!$this->checkAndProcessType ($getType, $getId)) {
Minz_Log::record ('Not found [' . $getType . '][' . $getId . ']', Minz_Log::DEBUG);
Minz_Error::error (
404,
array ('error' => array (Minz_Translate::t ('page_not_found')))
);
/**
* This action displays the normal view of FreshRSS.
*/
public function normalAction() {
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) {
Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
return;
}
// mise à jour des titres
$this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
if ($this->view->nb_not_read > 0) {
Minz_View::appendTitle (' (' . formatNumber($this->view->nb_not_read) . ')');
try {
$this->updateContext();
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
Minz_View::prependTitle (
$this->view->currentName .
($this->nb_not_read_cat > 0 ? ' (' . formatNumber($this->nb_not_read_cat) . ')' : '') .
' · '
);
// On récupère les différents éléments de filtrage
$this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view);
$filter = Minz_Request::param ('search', '');
if (!empty($filter)) {
$state = 'all'; //Search always in read and unread articles
}
$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', '');
$this->view->callbackBeforeContent = function($view) {
try {
FreshRSS_Context::$number++; //+1 for pagination
$entries = FreshRSS_index_Controller::listEntriesByContext();
FreshRSS_Context::$number--;
if ($state === 'not_read') { //Any unread article in this category at all?
switch ($getType) {
case 'a':
$hasUnread = $this->view->nb_not_read > 0;
break;
case 's':
$hasUnread = $this->view->nb_favorites['unread'] > 0;
break;
case 'c':
$hasUnread = (!isset($this->view->cat_aside[$getId])) || ($this->view->cat_aside[$getId]->nbNotRead() > 0);
break;
case 'f':
$myFeed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
$hasUnread = ($myFeed === null) || ($myFeed->nbNotRead() > 0);
break;
default:
$hasUnread = true;
break;
$nb_entries = count($entries);
if ($nb_entries > FreshRSS_Context::$number) {
// We have more elements for pagination
$last_entry = array_pop($entries);
FreshRSS_Context::$next_id = $last_entry->id();
}
$first_entry = $nb_entries > 0 ? $entries[0] : null;
FreshRSS_Context::$id_max = $first_entry === null ?
(time() - 1) . '000000' :
$first_entry->id();
if (FreshRSS_Context::$order === 'ASC') {
// In this case we do not know but we guess id_max
$id_max = (time() - 1) . '000000';
if (strcmp($id_max, FreshRSS_Context::$id_max) > 0) {
FreshRSS_Context::$id_max = $id_max;
}
}
$view->entries = $entries;
} catch (FreshRSS_EntriesGetter_Exception $e) {
Minz_Log::notice($e->getMessage());
Minz_Error::error(404);
}
if (!$hasUnread) {
$this->view->state = $state = 'all';
$view->categories = FreshRSS_Context::$categories;
$view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
$title = FreshRSS_Context::$name;
if (FreshRSS_Context::$get_unread > 0) {
$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
}
Minz_View::prependTitle($title . ' · ');
};
}
/**
* This action displays the reader view of FreshRSS.
*
* @todo: change this view into specific CSS rules?
*/
public function readerAction() {
$this->normalAction();
}
/**
* This action displays the global view of FreshRSS.
*/
public function globalAction() {
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) {
Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
return;
}
$today = @strtotime('today');
$this->view->today = $today;
// on calcule la date des articles les plus anciens qu'on affiche
$nb_month_old = $this->view->conf->old_entries;
$date_min = $today - (3600 * 24 * 30 * $nb_month_old); //Do not use a fast changing value such as time() to allow SQL caching
$keepHistoryDefault = $this->view->conf->keep_history_default;
Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
try {
$entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, $keepHistoryDefault);
// Si on a récupéré aucun article "non lus"
// on essaye de récupérer tous les articles
if ($state === 'not_read' && empty($entries)) {
Minz_Log::record ('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
$this->view->state = 'all';
$entries = $entryDAO->listWhere($getType, $getId, 'all', $order, $nb, $first, $filter, $date_min, $keepHistoryDefault);
}
if (count($entries) <= $nb) {
$this->view->nextId = '';
} else { //We have more elements for pagination
$lastEntry = array_pop($entries);
$this->view->nextId = $lastEntry->id();
}
$this->view->entries = $entries;
} catch (FreshRSS_EntriesGetter_Exception $e) {
Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
Minz_Error::error (
404,
array ('error' => array (Minz_Translate::t ('page_not_found')))
);
$this->updateContext();
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
$this->view->categories = FreshRSS_Context::$categories;
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
$title = _t('index.feed.title_global');
if (FreshRSS_Context::$get_unread > 0) {
$title = '(' . FreshRSS_Context::$get_unread . ') ' . $title;
}
Minz_View::prependTitle($title . ' · ');
}
/*
* Vérifie que la catégorie / flux sélectionné existe
* + Initialise correctement les variables de vue get_c et get_f
* + Met à jour la variable $this->nb_not_read_cat
/**
* This action displays the RSS feed of FreshRSS.
*/
private function checkAndProcessType ($getType, $getId) {
switch ($getType) {
case 'a':
$this->view->currentName = Minz_Translate::t ('your_rss_feeds');
$this->nb_not_read_cat = $this->view->nb_not_read;
$this->view->get_c = $getType;
return true;
case 's':
$this->view->currentName = Minz_Translate::t ('your_favorites');
$this->nb_not_read_cat = $this->view->nb_favorites['unread'];
$this->view->get_c = $getType;
return true;
case 'c':
$cat = isset($this->view->cat_aside[$getId]) ? $this->view->cat_aside[$getId] : null;
if ($cat === null) {
$catDAO = new FreshRSS_CategoryDAO();
$cat = $catDAO->searchById($getId);
}
if ($cat) {
$this->view->currentName = $cat->name ();
$this->nb_not_read_cat = $cat->nbNotRead ();
$this->view->get_c = $getId;
return true;
} else {
return false;
}
case 'f':
$feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
if (empty($feed)) {
$feedDAO = new FreshRSS_FeedDAO();
$feed = $feedDAO->searchById($getId);
}
if ($feed) {
$this->view->currentName = $feed->name ();
$this->nb_not_read_cat = $feed->nbNotRead ();
$this->view->get_f = $getId;
$this->view->get_c = $feed->category ();
return true;
} else {
return false;
}
default:
return false;
public function rssAction() {
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
$token = FreshRSS_Context::$user_conf->token;
$token_param = Minz_Request::param('token', '');
$token_is_ok = ($token != '' && $token === $token_param);
// Check if user has access.
if (!FreshRSS_Auth::hasAccess() &&
!$allow_anonymous &&
!$token_is_ok) {
Minz_Error::error(403);
}
try {
$this->updateContext();
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
try {
$this->view->entries = FreshRSS_index_Controller::listEntriesByContext();
} catch (FreshRSS_EntriesGetter_Exception $e) {
Minz_Log::notice($e->getMessage());
Minz_Error::error(404);
}
// No layout for RSS output.
$this->view->url = empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING'];
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
$this->view->_useLayout(false);
header('Content-Type: application/rss+xml; charset=utf-8');
}
public function statsAction () {
if (!$this->view->loginOk) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
/**
* This action updates the Context object by using request parameters.
*
* Parameters are:
* - state (default: conf->default_view)
* - search (default: empty string)
* - order (default: conf->sort_order)
* - nb (default: conf->posts_per_page)
* - next (default: empty string)
* - hours (default: 0)
*/
private function updateContext() {
if (empty(FreshRSS_Context::$categories)) {
$catDAO = new FreshRSS_CategoryDAO();
FreshRSS_Context::$categories = $catDAO->listCategories();
}
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites();
FreshRSS_Context::$total_unread = FreshRSS_CategoryDAO::CountUnreads(
FreshRSS_Context::$categories, 1
);
FreshRSS_Context::_get(Minz_Request::param('get', 'a'));
FreshRSS_Context::$state = Minz_Request::param(
'state', FreshRSS_Context::$user_conf->default_state
);
$state_forced_by_user = Minz_Request::param('state', false) !== false;
if (FreshRSS_Context::$user_conf->default_view === 'adaptive' &&
FreshRSS_Context::$get_unread <= 0 &&
!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ) &&
!$state_forced_by_user) {
FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ;
}
FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
FreshRSS_Context::$order = Minz_Request::param(
'order', FreshRSS_Context::$user_conf->sort_order
);
FreshRSS_Context::$number = intval(Minz_Request::param('nb', FreshRSS_Context::$user_conf->posts_per_page));
if (FreshRSS_Context::$number > FreshRSS_Context::$user_conf->max_posts_per_rss) {
FreshRSS_Context::$number = max(
FreshRSS_Context::$user_conf->max_posts_per_rss,
FreshRSS_Context::$user_conf->posts_per_page);
}
FreshRSS_Context::$first_id = Minz_Request::param('next', '');
FreshRSS_Context::$sinceHours = intval(Minz_Request::param('hours', 0));
}
/**
* This method returns a list of entries based on the Context object.
*/
public static function listEntriesByContext() {
$entryDAO = FreshRSS_Factory::createEntryDao();
$get = FreshRSS_Context::currentGet(true);
if (count($get) > 1) {
$type = $get[0];
$id = $get[1];
} else {
$type = $get;
$id = '';
}
$limit = FreshRSS_Context::$number;
$date_min = 0;
if (FreshRSS_Context::$sinceHours) {
$date_min = time() - (FreshRSS_Context::$sinceHours * 3600);
$limit = FreshRSS_Context::$user_conf->max_posts_per_rss;
}
$entries = $entryDAO->listWhere(
$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
$limit, FreshRSS_Context::$first_id,
FreshRSS_Context::$search, $date_min
);
if (FreshRSS_Context::$sinceHours && (count($entries) < FreshRSS_Context::$user_conf->min_posts_per_rss)) {
$date_min = 0;
$limit = FreshRSS_Context::$user_conf->min_posts_per_rss;
$entries = $entryDAO->listWhere(
$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
$limit, FreshRSS_Context::$first_id,
FreshRSS_Context::$search, $date_min
);
}
Minz_View::prependTitle (Minz_Translate::t ('stats') . ' · ');
$statsDAO = new FreshRSS_StatsDAO ();
Minz_View::appendScript (Minz_Url::display ('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
$this->view->repartition = $statsDAO->calculateEntryRepartition();
$this->view->count = ($statsDAO->calculateEntryCount());
$this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
$this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
$this->view->topFeed = $statsDAO->calculateTopFeed();
return $entries;
}
public function aboutAction () {
Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
/**
* This action displays the about page of FreshRSS.
*/
public function aboutAction() {
Minz_View::prependTitle(_t('index.about.title') . ' · ');
}
public function logsAction () {
if (!$this->view->loginOk) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
/**
* This action displays logs of FreshRSS for the current user.
*/
public function logsAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · ');
Minz_View::prependTitle(_t('index.log.title') . ' · ');
if (Minz_Request::isPost ()) {
if (Minz_Request::isPost()) {
FreshRSS_LogDAO::truncate();
}
$logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines
//gestion pagination
$page = Minz_Request::param ('page', 1);
$this->view->logsPaginator = new Minz_Paginator ($logs);
$this->view->logsPaginator->_nbItemsPerPage (50);
$this->view->logsPaginator->_currentPage ($page);
}
public function loginAction () {
$this->view->_useLayout (false);
$url = 'https://verifier.login.persona.org/verify';
$assert = Minz_Request::param ('assertion');
$params = 'assertion=' . $assert . '&audience=' .
urlencode (Minz_Url::display (null, 'php', true));
$ch = curl_init ();
$options = array (
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_POST => 2,
CURLOPT_POSTFIELDS => $params
);
curl_setopt_array ($ch, $options);
$result = curl_exec ($ch);
curl_close ($ch);
$res = json_decode ($result, true);
$loginOk = false;
$reason = '';
if ($res['status'] === 'okay') {
$email = filter_var($res['email'], FILTER_VALIDATE_EMAIL);
if ($email != '') {
$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
if (($currentUser = @file_get_contents($personaFile)) !== false) {
$currentUser = trim($currentUser);
if (ctype_alnum($currentUser)) {
try {
$this->conf = new FreshRSS_Configuration($currentUser);
$loginOk = strcasecmp($email, $this->conf->mail_login) === 0;
} catch (Minz_Exception $e) {
$reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage(); //Permission denied or conf file does not exist
}
} else {
$reason = 'Invalid username format [' . $currentUser . ']!';
}
}
} else {
$reason = 'Invalid email format [' . $res['email'] . ']!';
}
}
if ($loginOk) {
Minz_Session::_param('currentUser', $currentUser);
Minz_Session::_param ('mail', $email);
$this->view->loginOk = true;
invalidateHttpCache();
} else {
$res = array ();
$res['status'] = 'failure';
$res['reason'] = $reason == '' ? Minz_Translate::t ('invalid_login') : $reason;
Minz_Log::record ('Persona: ' . $res['reason'], Minz_Log::WARNING);
}
header('Content-Type: application/json; charset=UTF-8');
$this->view->res = json_encode ($res);
}
public function logoutAction () {
$this->view->_useLayout(false);
invalidateHttpCache();
Minz_Session::_param('currentUser');
Minz_Session::_param('mail');
Minz_Session::_param('passwordHash');
}
public function formLoginAction () {
if (Minz_Request::isPost()) {
$ok = false;
$nonce = Minz_Session::param('nonce');
$username = Minz_Request::param('username', '');
$c = Minz_Request::param('challenge', '');
if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) {
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
try {
$conf = new FreshRSS_Configuration($username);
$s = $conf->passwordHash;
$ok = password_verify($nonce . $s, $c);
if ($ok) {
Minz_Session::_param('currentUser', $username);
Minz_Session::_param('passwordHash', $s);
} else {
Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING);
}
} catch (Minz_Exception $me) {
Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
}
} else {
Minz_Log::record('Invalid credential parameters: user=' . $username . ' challenge=' . $c . ' nonce=' . $nonce, Minz_Log::DEBUG);
}
if (!$ok) {
$notif = array(
'type' => 'bad',
'content' => Minz_Translate::t('invalid_login')
);
Minz_Session::_param('notification', $notif);
}
$this->view->_useLayout(false);
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
} elseif (!Minz_Configuration::canLogIn()) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
}
invalidateHttpCache();
}
public function formLogoutAction () {
$this->view->_useLayout(false);
invalidateHttpCache();
Minz_Session::_param('currentUser');
Minz_Session::_param('mail');
Minz_Session::_param('passwordHash');
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
$page = Minz_Request::param('page', 1);
$this->view->logsPaginator = new Minz_Paginator($logs);
$this->view->logsPaginator->_nbItemsPerPage(50);
$this->view->logsPaginator->_currentPage($page);
}
}

View File

@@ -1,14 +1,14 @@
<?php
class FreshRSS_javascript_Controller extends Minz_ActionController {
public function firstAction () {
$this->view->_useLayout (false);
public function firstAction() {
$this->view->_useLayout(false);
}
public function actualizeAction () {
header('Content-Type: text/javascript; charset=UTF-8');
$feedDAO = new FreshRSS_FeedDAO ();
$this->view->feeds = $feedDAO->listFeedsOrderUpdate();
public function actualizeAction() {
header('Content-Type: application/json; charset=UTF-8');
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
}
public function nbUnreadsPerFeedAction() {
@@ -28,19 +28,27 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
$user = isset($_GET['user']) ? $_GET['user'] : '';
if (ctype_alnum($user)) {
try {
$conf = new FreshRSS_Configuration($user);
$salt = FreshRSS_Context::$system_conf->salt;
$conf = get_user_configuration($user);
$s = $conf->passwordHash;
if (strlen($s) >= 60) {
$this->view->salt1 = substr($s, 0, 29); //CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z".
$this->view->nonce = sha1(Minz_Configuration::salt() . uniqid(mt_rand(), true));
$this->view->nonce = sha1($salt . uniqid(mt_rand(), true));
Minz_Session::_param('nonce', $this->view->nonce);
return; //Success
}
} catch (Minz_Exception $me) {
Minz_Log::record('Nonce failure: ' . $me->getMessage(), Minz_Log::WARNING);
Minz_Log::warning('Nonce failure: ' . $me->getMessage());
}
} else {
Minz_Log::notice('Nonce failure due to invalid username!');
}
$this->view->nonce = ''; //Failure
$this->view->salt1 = '';
//Failure: Return random data.
$this->view->salt1 = sprintf('$2a$%02d$', FreshRSS_user_Controller::BCRYPT_COST);
$alphabet = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for ($i = 22; $i > 0; $i--) {
$this->view->salt1 .= $alphabet[rand(0, 63)];
}
$this->view->nonce = sha1(rand());
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Controller to handle application statistics.
*/
class FreshRSS_stats_Controller extends Minz_ActionController {
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
Minz_View::prependTitle(_t('admin.stats.title') . ' · ');
}
private function convertToSerie($data) {
$serie = array();
foreach ($data as $key => $value) {
$serie[] = array($key, $value);
}
return $serie;
}
private function convertToPieSerie($data) {
$serie = array();
foreach ($data as $value) {
$value['data'] = array(array(0, (int) $value['data']));
$serie[] = $value;
}
return $serie;
}
/**
* This action handles the statistic main page.
*
* It displays the statistic main page.
* The values computed to display the page are:
* - repartition of read/unread/favorite/not favorite
* - number of article per day
* - number of feed by category
* - number of article by category
* - list of most prolific feed
*/
public function indexAction() {
$statsDAO = FreshRSS_Factory::createStatsDAO();
Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
$this->view->repartition = $statsDAO->calculateEntryRepartition();
$entryCount = $statsDAO->calculateEntryCount();
$this->view->count = $this->convertToSerie($entryCount);
$this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2);
$this->view->feedByCategory = $this->convertToPieSerie($statsDAO->calculateFeedByCategory());
$this->view->entryByCategory = $this->convertToPieSerie($statsDAO->calculateEntryByCategory());
$this->view->topFeed = $statsDAO->calculateTopFeed();
}
/**
* This action handles the idle feed statistic page.
*
* It displays the list of idle feed for different period. The supported
* periods are:
* - last year
* - last 6 months
* - last 3 months
* - last month
* - last week
*/
public function idleAction() {
$statsDAO = FreshRSS_Factory::createStatsDAO();
$feeds = $statsDAO->calculateFeedLastDate();
$idleFeeds = array(
'last_year' => array(),
'last_6_month' => array(),
'last_3_month' => array(),
'last_month' => array(),
'last_week' => array(),
);
$now = new \DateTime();
$feedDate = clone $now;
$lastWeek = clone $now;
$lastWeek->modify('-1 week');
$lastMonth = clone $now;
$lastMonth->modify('-1 month');
$last3Month = clone $now;
$last3Month->modify('-3 month');
$last6Month = clone $now;
$last6Month->modify('-6 month');
$lastYear = clone $now;
$lastYear->modify('-1 year');
foreach ($feeds as $feed) {
$feedDate->setTimestamp($feed['last_date']);
if ($feedDate >= $lastWeek) {
continue;
}
if ($feedDate < $lastYear) {
$idleFeeds['last_year'][] = $feed;
} elseif ($feedDate < $last6Month) {
$idleFeeds['last_6_month'][] = $feed;
} elseif ($feedDate < $last3Month) {
$idleFeeds['last_3_month'][] = $feed;
} elseif ($feedDate < $lastMonth) {
$idleFeeds['last_month'][] = $feed;
} elseif ($feedDate < $lastWeek) {
$idleFeeds['last_week'][] = $feed;
}
}
$this->view->idleFeeds = $idleFeeds;
}
/**
* This action handles the article repartition statistic page.
*
* It displays the number of article and the average of article for the
* following periods:
* - hour of the day
* - day of the week
* - month
*
* @todo verify that the metrics used here make some sense. Especially
* for the average.
*/
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->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id);
$this->view->repartitionHour = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerHour($id));
$this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id);
$this->view->repartitionDayOfWeek = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id));
$this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id);
$this->view->repartitionMonth = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerMonth($id));
$this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* Controller to handle subscription actions.
*/
class FreshRSS_subscription_Controller extends Minz_ActionController {
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
$catDAO = new FreshRSS_CategoryDAO();
$catDAO->checkDefault();
$this->view->categories = $catDAO->listCategories(false);
$this->view->default_category = $catDAO->getDefault();
}
/**
* This action handles the main subscription page
*
* It displays categories and associated feeds.
*/
public function indexAction() {
Minz_View::appendScript(Minz_Url::display('/scripts/category.js?' .
@filemtime(PUBLIC_PATH . '/scripts/category.js')));
Minz_View::prependTitle(_t('sub.title') . ' · ');
$id = Minz_Request::param('id');
if ($id !== false) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feed = $feedDAO->searchById($id);
}
}
/**
* This action handles the feed configuration page.
*
* It displays the feed configuration page.
* If this action is reached through a POST request, it stores all new
* configuraiton values then sends a notification to the user.
*
* The options available on the page are:
* - name
* - description
* - website URL
* - feed URL
* - category id (default: default category id)
* - CSS path to article on website
* - display in main stream (default: 0)
* - HTTP authentication
* - number of article to retain (default: -2)
* - refresh frequency (default: -2)
* Default values are empty strings unless specified.
*/
public function feedAction() {
if (Minz_Request::param('ajax')) {
$this->view->_useLayout(false);
}
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feeds = $feedDAO->listFeeds();
$id = Minz_Request::param('id');
if ($id === false || !isset($this->view->feeds[$id])) {
Minz_Error::error(404);
return;
}
$this->view->feed = $this->view->feeds[$id];
Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · ');
if (Minz_Request::isPost()) {
$user = trim(Minz_Request::param('http_user_feed' . $id, ''));
$pass = Minz_Request::param('http_pass_feed' . $id, '');
$httpAuth = '';
if ($user != '' && $pass != '') { //TODO: Sanitize
$httpAuth = $user . ':' . $pass;
}
$cat = intval(Minz_Request::param('category', 0));
$values = array(
'name' => Minz_Request::param('name', ''),
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
'website' => Minz_Request::param('website', ''),
'url' => Minz_Request::param('url', ''),
'category' => $cat,
'pathEntries' => Minz_Request::param('path_entries', ''),
'priority' => intval(Minz_Request::param('priority', 0)),
'httpAuth' => $httpAuth,
'keep_history' => intval(Minz_Request::param('keep_history', -2)),
'ttl' => intval(Minz_Request::param('ttl', -2)),
);
invalidateHttpCache();
$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id));
if ($feedDAO->updateFeed($id, $values) !== false) {
$this->view->feed->_category($cat);
$this->view->feed->faviconPrepare();
Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
} else {
Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
}
}
}
}

View File

@@ -0,0 +1,226 @@
<?php
class FreshRSS_update_Controller extends Minz_ActionController {
public static function isGit() {
return is_dir(FRESHRSS_PATH . '/.git/');
}
public static function hasGitUpdate() {
$cwd = getcwd();
chdir(FRESHRSS_PATH);
$output = array();
try {
exec('git fetch', $output, $return);
if ($return == 0) {
exec('git status -sb --porcelain remote', $output, $return);
} else {
$line = is_array($output) ? implode('; ', $output) : '' . $output;
Minz_Log::warning('git fetch warning:' . $line);
}
} catch (Exception $e) {
Minz_Log::warning('git fetch error:' . $e->getMessage());
}
chdir($cwd);
$line = is_array($output) ? implode('; ', $output) : '' . $output;
return strpos($line, '[behind') !== false;
}
public static function gitPull() {
$cwd = getcwd();
chdir(FRESHRSS_PATH);
$output = array();
$return = 1;
try {
exec('git pull --ff-only', $output, $return);
} catch (Exception $e) {
Minz_Log::warning('git pull error:' . $e->getMessage());
}
chdir($cwd);
$line = is_array($output) ? implode('; ', $output) : '' . $output;
return $return == 0 ? true : 'Git error: ' . $line;
}
public function firstAction() {
if (!FreshRSS_Auth::hasAccess('admin')) {
Minz_Error::error(403);
}
invalidateHttpCache();
$this->view->update_to_apply = false;
$this->view->last_update_time = 'unknown';
$timestamp = @filemtime(join_path(DATA_PATH, 'last_update.txt'));
if ($timestamp !== false) {
$this->view->last_update_time = timestamptodate($timestamp);
}
}
public function indexAction() {
Minz_View::prependTitle(_t('admin.update.title') . ' · ');
if (!is_writable(FRESHRSS_PATH)) {
$this->view->message = array(
'status' => 'bad',
'title' => _t('gen.short.damn'),
'body' => _t('feedback.update.file_is_nok', FRESHRSS_PATH)
);
} elseif (file_exists(UPDATE_FILENAME)) {
// There is an update file to apply!
$version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt'));
if (empty($version)) {
$version = 'unknown';
}
$this->view->update_to_apply = true;
$this->view->message = array(
'status' => 'good',
'title' => _t('gen.short.ok'),
'body' => _t('feedback.update.can_apply', $version)
);
}
}
public function checkAction() {
$this->view->change_view('update', 'index');
if (file_exists(UPDATE_FILENAME)) {
// There is already an update file to apply: we don't need to check
// the webserver!
// Or if already check during the last hour, do nothing.
Minz_Request::forward(array('c' => 'update'), true);
return;
}
$script = '';
$version = '';
if (self::isGit()) {
if (self::hasGitUpdate()) {
$version = 'git';
} else {
$this->view->message = array(
'status' => 'bad',
'title' => _t('gen.short.damn'),
'body' => _t('feedback.update.none')
);
@touch(join_path(DATA_PATH, 'last_update.txt'));
return;
}
} else {
$auto_update_url = FreshRSS_Context::$system_conf->auto_update_url . '?v=' . FRESHRSS_VERSION;
Minz_Log::debug('HTTP GET ' . $auto_update_url);
$c = curl_init($auto_update_url);
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 2);
$result = curl_exec($c);
$c_status = curl_getinfo($c, CURLINFO_HTTP_CODE);
$c_error = curl_error($c);
curl_close($c);
if ($c_status !== 200) {
Minz_Log::warning(
'Error during update (HTTP code ' . $c_status . '): ' . $c_error
);
$this->view->message = array(
'status' => 'bad',
'title' => _t('gen.short.damn'),
'body' => _t('feedback.update.server_not_found', $auto_update_url)
);
return;
}
$res_array = explode("\n", $result, 2);
$status = $res_array[0];
if (strpos($status, 'UPDATE') !== 0) {
$this->view->message = array(
'status' => 'bad',
'title' => _t('gen.short.damn'),
'body' => _t('feedback.update.none')
);
@touch(join_path(DATA_PATH, 'last_update.txt'));
return;
}
$script = $res_array[1];
$version = explode(' ', $status, 2);
$version = $version[1];
}
if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
@file_put_contents(join_path(DATA_PATH, 'last_update.txt'), $version);
Minz_Request::forward(array('c' => 'update'), true);
} else {
$this->view->message = array(
'status' => 'bad',
'title' => _t('gen.short.damn'),
'body' => _t('feedback.update.error', 'Cannot save the update script')
);
}
}
public function applyAction() {
if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH)) {
Minz_Request::forward(array('c' => 'update'), true);
}
if (Minz_Request::param('post_conf', false)) {
if (self::isGit()) {
$res = !self::hasGitUpdate();
} else {
require(UPDATE_FILENAME);
$res = do_post_update();
}
Minz_ExtensionManager::callHook('post_update');
if ($res === true) {
@unlink(UPDATE_FILENAME);
@file_put_contents(join_path(DATA_PATH, 'last_update.txt'), '');
Minz_Request::good(_t('feedback.update.finished'));
} else {
Minz_Request::bad(_t('feedback.update.error', $res),
array('c' => 'update', 'a' => 'index'));
}
} else {
$res = false;
if (self::isGit()) {
$res = self::gitPull();
} else {
if (Minz_Request::isPost()) {
save_info_update();
}
if (!need_info_update()) {
$res = apply_update();
} else {
return;
}
}
if ($res === true) {
Minz_Request::forward(array(
'c' => 'update',
'a' => 'apply',
'params' => array('post_conf' => true)
), true);
} else {
Minz_Request::bad(_t('feedback.update.error', $res),
array('c' => 'update', 'a' => 'index'));
}
}
}
/**
* This action displays information about installation.
*/
public function checkInstallAction() {
Minz_View::prependTitle(_t('admin.check_install.title') . ' · ');
$this->view->status_php = check_install_php();
$this->view->status_files = check_install_files();
$this->view->status_database = check_install_database();
}
}

View File

@@ -0,0 +1,257 @@
<?php
/**
* Controller to handle user actions.
*/
class FreshRSS_user_Controller extends Minz_ActionController {
// Will also have to be computed client side on mobile devices,
// so do not use a too high cost
const BCRYPT_COST = 9;
/**
* This action is called before every other action in that class. It is
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*
* @todo clean up the access condition.
*/
public function firstAction() {
if (!FreshRSS_Auth::hasAccess() && !(
Minz_Request::actionName() === 'create' &&
!max_registrations_reached()
)) {
Minz_Error::error(403);
}
}
public static function hashPassword($passwordPlain) {
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
$passwordPlain = '';
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
return $passwordHash == '' ? '' : $passwordHash;
}
/**
* This action displays the user profile page.
*/
public function profileAction() {
Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
Minz_View::appendScript(Minz_Url::display(
'/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
));
if (Minz_Request::isPost()) {
$ok = true;
$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
if ($passwordPlain != '') {
Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP
$_POST['newPasswordPlain'] = '';
$passwordHash = self::hashPassword($passwordPlain);
$ok &= ($passwordHash != '');
FreshRSS_Context::$user_conf->passwordHash = $passwordHash;
}
Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
$passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
if ($passwordPlain != '') {
$passwordHash = self::hashPassword($passwordPlain);
$ok &= ($passwordHash != '');
FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash;
}
$ok &= FreshRSS_Context::$user_conf->save();
if ($ok) {
Minz_Request::good(_t('feedback.profile.updated'),
array('c' => 'user', 'a' => 'profile'));
} else {
Minz_Request::bad(_t('feedback.profile.error'),
array('c' => 'user', 'a' => 'profile'));
}
}
}
/**
* This action displays the user management page.
*/
public function manageAction() {
if (!FreshRSS_Auth::hasAccess('admin')) {
Minz_Error::error(403);
}
Minz_View::prependTitle(_t('admin.user.title') . ' · ');
// Get the correct current user.
$username = Minz_Request::param('u', Minz_Session::param('currentUser'));
if (!FreshRSS_UserDAO::exist($username)) {
$username = Minz_Session::param('currentUser');
}
$this->view->current_user = $username;
// Get information about the current user.
$entryDAO = FreshRSS_Factory::createEntryDao($this->view->current_user);
$this->view->nb_articles = $entryDAO->count();
$this->view->size_user = $entryDAO->size();
}
public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) {
if (!is_array($userConfig)) {
$userConfig = array();
}
$ok = ($new_user_name != '') && ctype_alnum($new_user_name);
if ($ok) {
$languages = Minz_Translate::availableLanguages();
if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages)) {
$userConfig['language'] = 'en';
}
$ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive
$configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php');
$ok &= !file_exists($configPath);
}
if ($ok) {
$passwordHash = '';
if ($passwordPlain != '') {
$passwordHash = self::hashPassword($passwordPlain);
$ok &= ($passwordHash != '');
}
$apiPasswordHash = '';
if ($apiPasswordPlain != '') {
$apiPasswordHash = self::hashPassword($apiPasswordPlain);
$ok &= ($apiPasswordHash != '');
}
}
if ($ok) {
mkdir(join_path(DATA_PATH, 'users', $new_user_name));
$userConfig['passwordHash'] = $passwordHash;
$userConfig['apiPasswordHash'] = $apiPasswordHash;
$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
}
if ($ok) {
$userDAO = new FreshRSS_UserDAO();
$ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
}
return $ok;
}
/**
* This action creates a new user.
*
* Request parameters are:
* - new_user_language
* - new_user_name
* - new_user_passwordPlain
* - r (i.e. a redirection url, optional)
*
* @todo clean up this method. Idea: write a method to init a user with basic information.
* @todo handle r redirection in Minz_Request::forward directly?
*/
public function createAction() {
if (Minz_Request::isPost() && (
FreshRSS_Auth::hasAccess('admin') ||
!max_registrations_reached()
)) {
$new_user_name = Minz_Request::param('new_user_name');
$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
$new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
$ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language));
Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
$_POST['new_user_passwordPlain'] = '';
invalidateHttpCache();
$notif = array(
'type' => $ok ? 'good' : 'bad',
'content' => _t('feedback.user.created' . (!$ok ? '.error' : ''), $new_user_name)
);
Minz_Session::_param('notification', $notif);
}
$redirect_url = urldecode(Minz_Request::param('r', false, true));
if (!$redirect_url) {
$redirect_url = array('c' => 'user', 'a' => 'manage');
}
Minz_Request::forward($redirect_url, true);
}
public static function deleteUser($username) {
$db = FreshRSS_Context::$system_conf->db;
require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
$ok = ctype_alnum($username);
if ($ok) {
$default_user = FreshRSS_Context::$system_conf->default_user;
$ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user
}
$user_data = join_path(DATA_PATH, 'users', $username);
if ($ok) {
$ok &= is_dir($user_data);
}
if ($ok) {
$userDAO = new FreshRSS_UserDAO();
$ok &= $userDAO->deleteUser($username);
$ok &= recursive_unlink($user_data);
}
return $ok;
}
/**
* This action delete an existing user.
*
* Request parameter is:
* - username
*
* @todo clean up this method. Idea: create a User->clean() method.
*/
public function deleteAction() {
$username = Minz_Request::param('username');
$redirect_url = urldecode(Minz_Request::param('r', false, true));
if (!$redirect_url) {
$redirect_url = array('c' => 'user', 'a' => 'manage');
}
$self_deletion = Minz_Session::param('currentUser', '_') === $username;
if (Minz_Request::isPost() && (
FreshRSS_Auth::hasAccess('admin') ||
$self_deletion
)) {
$ok = true;
if ($ok && $self_deletion) {
// We check the password if it's a self-destruction
$nonce = Minz_Session::param('nonce');
$challenge = Minz_Request::param('challenge', '');
$ok &= FreshRSS_FormAuth::checkCredentials(
$username, FreshRSS_Context::$user_conf->passwordHash,
$nonce, $challenge
);
}
if ($ok) {
$ok &= self::deleteUser($username);
}
if ($ok && $self_deletion) {
FreshRSS_Auth::removeAccess();
$redirect_url = array('c' => 'index', 'a' => 'index');
}
invalidateHttpCache();
$notif = array(
'type' => $ok ? 'good' : 'bad',
'content' => _t('feedback.user.deleted' . (!$ok ? '.error' : ''), $username)
);
Minz_Session::_param('notification', $notif);
}
Minz_Request::forward($redirect_url, true);
}
}

View File

@@ -1,183 +0,0 @@
<?php
class FreshRSS_users_Controller extends Minz_ActionController {
const BCRYPT_COST = 9; //Will also have to be computed client side on mobile devices, so do not use a too high cost
public function firstAction() {
if (!$this->view->loginOk) {
Minz_Error::error(
403,
array('error' => array(Minz_Translate::t('access_denied')))
);
}
}
public function authAction() {
if (Minz_Request::isPost()) {
$ok = true;
$passwordPlain = Minz_Request::param('passwordPlain', false);
if ($passwordPlain != '') {
Minz_Request::_param('passwordPlain'); //Discard plain-text password ASAP
$_POST['passwordPlain'] = '';
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
$passwordPlain = '';
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
$ok &= ($passwordHash != '');
$this->view->conf->_passwordHash($passwordHash);
}
Minz_Session::_param('passwordHash', $this->view->conf->passwordHash);
if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
$this->view->conf->_mail_login(Minz_Request::param('mail_login', false));
}
$email = $this->view->conf->mail_login;
Minz_Session::_param('mail', $email);
$ok &= $this->view->conf->save();
if ($email != '') {
$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
@unlink($personaFile);
$ok &= (file_put_contents($personaFile, Minz_Session::param('currentUser', '_')) !== false);
}
if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
$current_token = $this->view->conf->token;
$token = Minz_Request::param('token', $current_token);
$this->view->conf->_token($token);
$ok &= $this->view->conf->save();
$anon = Minz_Request::param('anon_access', false);
$anon = ((bool)$anon) && ($anon !== 'no');
$anon_refresh = Minz_Request::param('anon_refresh', false);
$anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no');
$auth_type = Minz_Request::param('auth_type', 'none');
if ($anon != Minz_Configuration::allowAnonymous() ||
$auth_type != Minz_Configuration::authType() ||
$anon_refresh != Minz_Configuration::allowAnonymousRefresh()) {
Minz_Configuration::_authType($auth_type);
Minz_Configuration::_allowAnonymous($anon);
Minz_Configuration::_allowAnonymousRefresh($anon_refresh);
$ok &= Minz_Configuration::writeFile();
}
}
invalidateHttpCache();
$notif = array(
'type' => $ok ? 'good' : 'bad',
'content' => Minz_Translate::t($ok ? 'configuration_updated' : 'error_occurred')
);
Minz_Session::_param('notification', $notif);
}
Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
}
public function createAction() {
if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
require_once(APP_PATH . '/sql.php');
$new_user_language = Minz_Request::param('new_user_language', $this->view->conf->language);
if (!in_array($new_user_language, $this->view->conf->availableLanguages())) {
$new_user_language = $this->view->conf->language;
}
$new_user_name = Minz_Request::param('new_user_name');
$ok = ($new_user_name != '') && ctype_alnum($new_user_name);
if ($ok) {
$ok &= (strcasecmp($new_user_name, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to alter the default user
$ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive
$configPath = DATA_PATH . '/' . $new_user_name . '_user.php';
$ok &= !file_exists($configPath);
}
if ($ok) {
$passwordPlain = Minz_Request::param('new_user_passwordPlain', false);
$passwordHash = '';
if ($passwordPlain != '') {
Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
$_POST['new_user_passwordPlain'] = '';
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
$passwordPlain = '';
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
$ok &= ($passwordHash != '');
}
if (empty($passwordHash)) {
$passwordHash = '';
}
$new_user_email = filter_var($_POST['new_user_email'], FILTER_VALIDATE_EMAIL);
if (empty($new_user_email)) {
$new_user_email = '';
} else {
$personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt';
@unlink($personaFile);
$ok &= (file_put_contents($personaFile, $new_user_name) !== false);
}
}
if ($ok) {
$config_array = array(
'language' => $new_user_language,
'passwordHash' => $passwordHash,
'mail_login' => $new_user_email,
);
$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($config_array, true) . ';') !== false);
}
if ($ok) {
$userDAO = new FreshRSS_UserDAO();
$ok &= $userDAO->createUser($new_user_name);
}
invalidateHttpCache();
$notif = array(
'type' => $ok ? 'good' : 'bad',
'content' => Minz_Translate::t($ok ? 'user_created' : 'error_occurred', $new_user_name)
);
Minz_Session::_param('notification', $notif);
}
Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
}
public function deleteAction() {
if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
require_once(APP_PATH . '/sql.php');
$username = Minz_Request::param('username');
$ok = ctype_alnum($username);
if ($ok) {
$ok &= (strcasecmp($username, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to delete the default user
}
if ($ok) {
$configPath = DATA_PATH . '/' . $username . '_user.php';
$ok &= file_exists($configPath);
}
if ($ok) {
$userDAO = new FreshRSS_UserDAO();
$ok &= $userDAO->deleteUser($username);
$ok &= unlink($configPath);
//TODO: delete Persona file
}
invalidateHttpCache();
$notif = array(
'type' => $ok ? 'good' : 'bad',
'content' => Minz_Translate::t($ok ? 'user_deleted' : 'error_occurred', $username)
);
Minz_Session::_param('notification', $notif);
}
Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
}
}

View File

@@ -0,0 +1,14 @@
<?php
class FreshRSS_AlreadySubscribed_Exception extends Exception {
private $feedName = '';
public function __construct($url, $feedName) {
parent::__construct('Already subscribed! ' . $url, 2135);
$this->feedName = $feedName;
}
public function feedName() {
return $this->feedName;
}
}

View File

@@ -1,6 +1,9 @@
<?php
class FreshRSS_BadUrl_Exception extends FreshRSS_Feed_Exception {
public function __construct ($url) {
parent::__construct ('`' . $url . '` is not a valid URL');
public function __construct($url) {
parent::__construct('`' . $url . '` is not a valid URL');
}
}

View File

@@ -0,0 +1,8 @@
<?php
/**
* An exception raised when a context is invalid
*/
class FreshRSS_Context_Exception extends Exception {
}

View File

@@ -0,0 +1,5 @@
<?php
class FreshRSS_DAO_Exception extends Exception {
}

View File

@@ -1,7 +1,5 @@
<?php
class FreshRSS_EntriesGetter_Exception extends Exception {
public function __construct ($message) {
parent::__construct ($message);
}
}

View File

@@ -1,6 +1,5 @@
<?php
class FreshRSS_Feed_Exception extends Exception {
public function __construct ($message) {
parent::__construct ($message);
}
}

View File

@@ -0,0 +1,14 @@
<?php
class FreshRSS_FeedNotAdded_Exception extends Exception {
private $feedName = '';
public function __construct($url, $feedName) {
parent::__construct('Feed not added! ' . $url, 2147);
$this->feedName = $feedName;
}
public function feedName() {
return $this->feedName;
}
}

View File

@@ -1,6 +0,0 @@
<?php
class FreshRSS_Opml_Exception extends FreshRSS_Feed_Exception {
public function __construct ($name_file) {
parent::__construct ('OPML file is invalid');
}
}

View File

@@ -0,0 +1,14 @@
<?php
class FreshRSS_Zip_Exception extends Exception {
private $zipErrorCode = 0;
public function __construct($zipErrorCode) {
parent::__construct('ZIP error! ' . $url, 2141);
$this->zipErrorCode = $zipErrorCode;
}
public function zipErrorCode() {
return $this->zipErrorCode;
}
}

View File

@@ -0,0 +1,4 @@
<?php
class FreshRSS_ZipMissing_Exception extends Exception {
}

View File

@@ -1,146 +1,128 @@
<?php
class FreshRSS extends Minz_FrontController {
/**
* Initialize the different FreshRSS / Minz components.
*
* PLEASE DON'T CHANGE THE ORDER OF INITIALIZATIONS UNLESS YOU KNOW WHAT
* YOU DO!!
*
* Here is the list of components:
* - Create a configuration setter and register it to system conf
* - Init extension manager and enable system extensions (has to be done asap)
* - Init authentication system
* - Init user configuration (need auth system)
* - Init FreshRSS context (need user conf)
* - Init i18n (need context)
* - Init sharing system (need user conf and i18n)
* - Init generic styles and scripts (need user conf)
* - Init notifications
* - Enable user extensions (need all the other initializations)
*/
public function init() {
if (!isset($_SESSION)) {
Minz_Session::init('FreshRSS');
}
$loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
$this->loadParamsView();
$this->loadStylesAndScripts($loginOk); //TODO: Do not load that when not needed, e.g. some Ajax requests
$this->loadNotifications();
}
private function accessControl($currentUser) {
if ($currentUser == '') {
switch (Minz_Configuration::authType()) {
case 'form':
$currentUser = Minz_Configuration::defaultUser();
Minz_Session::_param('passwordHash');
$loginOk = false;
break;
case 'http_auth':
$currentUser = httpAuthUser();
$loginOk = $currentUser != '';
break;
case 'persona':
$loginOk = false;
$email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL);
if ($email != '') { //TODO: Remove redundancy with indexController
$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
if (($currentUser = @file_get_contents($personaFile)) !== false) {
$currentUser = trim($currentUser);
$loginOk = true;
}
}
if (!$loginOk) {
$currentUser = Minz_Configuration::defaultUser();
}
break;
case 'none':
$currentUser = Minz_Configuration::defaultUser();
$loginOk = true;
break;
default:
$currentUser = Minz_Configuration::defaultUser();
$loginOk = false;
break;
}
} else {
$loginOk = true;
}
// Register the configuration setter for the system configuration
$configuration_setter = new FreshRSS_ConfigurationSetter();
$system_conf = Minz_Configuration::get('system');
$system_conf->_configurationSetter($configuration_setter);
if (!ctype_alnum($currentUser)) {
Minz_Session::_param('currentUser', '');
die('Invalid username [' . $currentUser . ']!');
}
// Load list of extensions and enable the "system" ones.
Minz_ExtensionManager::init();
try {
$this->conf = new FreshRSS_Configuration($currentUser);
Minz_View::_param ('conf', $this->conf);
Minz_Session::_param('currentUser', $currentUser);
} catch (Minz_Exception $me) {
$loginOk = false;
try {
$this->conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
Minz_View::_param('conf', $this->conf);
$notif = array(
'type' => 'bad',
'content' => 'Invalid configuration for user [' . $currentUser . ']!',
);
Minz_Session::_param ('notification', $notif);
Minz_Log::record ($notif['content'] . ' ' . $me->getMessage(), Minz_Log::WARNING);
Minz_Session::_param('currentUser', '');
} catch (Exception $e) {
die($e->getMessage());
}
}
// Auth has to be initialized before using currentUser session parameter
// because it's this part which create this parameter.
self::initAuth();
if ($loginOk) {
switch (Minz_Configuration::authType()) {
case 'form':
$loginOk = Minz_Session::param('passwordHash') === $this->conf->passwordHash;
break;
case 'http_auth':
$loginOk = strcasecmp($currentUser, httpAuthUser()) === 0;
break;
case 'persona':
$loginOk = strcasecmp(Minz_Session::param('mail'), $this->conf->mail_login) === 0;
break;
case 'none':
$loginOk = true;
break;
default:
$loginOk = false;
break;
}
}
Minz_View::_param ('loginOk', $loginOk);
return $loginOk;
}
// Then, register the user configuration and use the configuration setter
// created above.
$current_user = Minz_Session::param('currentUser', '_');
Minz_Configuration::register('user',
join_path(USERS_PATH, $current_user, 'config.php'),
join_path(USERS_PATH, '_', 'config.default.php'),
$configuration_setter);
private function loadParamsView () {
Minz_Session::_param ('language', $this->conf->language);
Minz_Translate::init();
$output = Minz_Request::param ('output', '');
if (($output === '') || ($output !== 'normal' && $output !== 'rss' && $output !== 'reader' && $output !== 'global')) {
$output = $this->conf->view_mode;
Minz_Request::_param ('output', $output);
// Finish to initialize the other FreshRSS / Minz components.
FreshRSS_Context::init();
self::initI18n();
self::loadNotifications();
// Enable extensions for the current (logged) user.
if (FreshRSS_Auth::hasAccess() || $system_conf->allow_anonymous) {
$ext_list = FreshRSS_Context::$user_conf->extensions_enabled;
Minz_ExtensionManager::enableByList($ext_list);
}
}
private function loadStylesAndScripts ($loginOk) {
$theme = FreshRSS_Themes::load($this->conf->theme);
private static function initAuth() {
FreshRSS_Auth::init();
if (Minz_Request::isPost() && !(is_referer_from_same_domain() && FreshRSS_Auth::isCsrfOk())) {
// Basic protection against XSRF attacks
FreshRSS_Auth::removeAccess();
$http_referer = empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER'];
Minz_Translate::init('en'); //TODO: Better choice of fallback language
Minz_Error::error(
403,
array('error' => array(
_t('feedback.access.denied'),
' [HTTP_REFERER=' . htmlspecialchars($http_referer) . ']'
))
);
}
}
private static function initI18n() {
Minz_Session::_param('language', FreshRSS_Context::$user_conf->language);
Minz_Translate::init(FreshRSS_Context::$user_conf->language);
}
public static function loadStylesAndScripts() {
$theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme);
if ($theme) {
foreach($theme['files'] as $file) {
Minz_View::appendStyle (Minz_Url::display ('/themes/' . $theme['id'] . '/' . $file . '?' . @filemtime(PUBLIC_PATH . '/themes/' . $theme['id'] . '/' . $file)));
if ($file[0] === '_') {
$theme_id = 'base-theme';
$filename = substr($file, 1);
} else {
$theme_id = $theme['id'];
$filename = $file;
}
$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
$url = '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
header('Link: <' . Minz_Url::display($url, '', 'root') . '>;rel=preload', false); //HTTP2
Minz_View::appendStyle(Minz_Url::display($url));
}
}
switch (Minz_Configuration::authType()) {
case 'form':
if (!$loginOk) {
Minz_View::appendScript(Minz_Url::display ('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
}
break;
case 'persona':
Minz_View::appendScript('https://login.persona.org/include.js');
break;
}
$includeLazyLoad = $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param ('output') === 'reader');
Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')), false, !$includeLazyLoad, !$includeLazyLoad);
if ($includeLazyLoad) {
Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.lazyload.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.lazyload.min.js')));
}
Minz_View::appendScript (Minz_Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
Minz_View::appendScript (Minz_Url::display ('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')));
Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
}
private function loadNotifications () {
$notif = Minz_Session::param ('notification');
private static function loadNotifications() {
$notif = Minz_Session::param('notification');
if ($notif) {
Minz_View::_param ('notification', $notif);
Minz_Session::_param ('notification');
Minz_View::_param('notification', $notif);
Minz_Session::_param('notification');
}
}
public static function preLayout() {
switch (Minz_Request::controllerName()) {
case 'index':
header("Content-Security-Policy: default-src 'self'; child-src *; frame-src *; img-src * data:; media-src *");
break;
case 'stats':
header("Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'");
break;
default:
header("Content-Security-Policy: default-src 'self'");
break;
}
header("X-Content-Type-Options: nosniff");
FreshRSS_Share::load(join_path(DATA_PATH, 'shares.php'));
self::loadStylesAndScripts();
}
}

264
app/Models/Auth.php Normal file
View File

@@ -0,0 +1,264 @@
<?php
/**
* This class handles all authentication process.
*/
class FreshRSS_Auth {
/**
* Determines if user is connected.
*/
private static $login_ok = false;
/**
* This method initializes authentication system.
*/
public static function init() {
self::$login_ok = Minz_Session::param('loginOk', false);
$current_user = Minz_Session::param('currentUser', '');
if ($current_user === '') {
$conf = Minz_Configuration::get('system');
$current_user = $conf->default_user;
Minz_Session::_param('currentUser', $current_user);
}
if (self::$login_ok) {
self::giveAccess();
} elseif (self::accessControl()) {
self::giveAccess();
FreshRSS_UserDAO::touch();
} else {
// Be sure all accesses are removed!
self::removeAccess();
}
}
/**
* This method checks if user is allowed to connect.
*
* Required session parameters are also set in this method (such as
* currentUser).
*
* @return boolean true if user can be connected, false else.
*/
private static function accessControl() {
$conf = Minz_Configuration::get('system');
$auth_type = $conf->auth_type;
switch ($auth_type) {
case 'form':
$credentials = FreshRSS_FormAuth::getCredentialsFromCookie();
$current_user = '';
if (isset($credentials[1])) {
$current_user = trim($credentials[0]);
Minz_Session::_param('currentUser', $current_user);
Minz_Session::_param('passwordHash', trim($credentials[1]));
}
return $current_user != '';
case 'http_auth':
$current_user = httpAuthUser();
$login_ok = $current_user != '';
if ($login_ok) {
Minz_Session::_param('currentUser', $current_user);
}
return $login_ok;
case 'none':
return true;
default:
// TODO load extension
return false;
}
}
/**
* Gives access to the current user.
*/
public static function giveAccess() {
$current_user = Minz_Session::param('currentUser');
$user_conf = get_user_configuration($current_user);
$system_conf = Minz_Configuration::get('system');
switch ($system_conf->auth_type) {
case 'form':
self::$login_ok = Minz_Session::param('passwordHash') === $user_conf->passwordHash;
break;
case 'http_auth':
self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0;
break;
case 'none':
self::$login_ok = true;
break;
default:
// TODO: extensions
self::$login_ok = false;
}
Minz_Session::_param('loginOk', self::$login_ok);
}
/**
* Returns if current user has access to the given scope.
*
* @param string $scope general (default) or admin
* @return boolean true if user has corresponding access, false else.
*/
public static function hasAccess($scope = 'general') {
$conf = Minz_Configuration::get('system');
$default_user = $conf->default_user;
$ok = self::$login_ok;
switch ($scope) {
case 'general':
break;
case 'admin':
$ok &= Minz_Session::param('currentUser') === $default_user;
break;
default:
$ok = false;
}
return $ok;
}
/**
* Removes all accesses for the current user.
*/
public static function removeAccess() {
Minz_Session::_param('loginOk');
self::$login_ok = false;
$conf = Minz_Configuration::get('system');
Minz_Session::_param('currentUser', $conf->default_user);
Minz_Session::_param('csrf');
switch ($conf->auth_type) {
case 'form':
Minz_Session::_param('passwordHash');
FreshRSS_FormAuth::deleteCookie();
break;
case 'http_auth':
case 'none':
// Nothing to do...
break;
default:
// TODO: extensions
}
}
/**
* Return if authentication is enabled on this instance of FRSS.
*/
public static function accessNeedsLogin() {
$conf = Minz_Configuration::get('system');
$auth_type = $conf->auth_type;
return $auth_type !== 'none';
}
/**
* Return if authentication requires a PHP action.
*/
public static function accessNeedsAction() {
$conf = Minz_Configuration::get('system');
$auth_type = $conf->auth_type;
return $auth_type === 'form';
}
public static function csrfToken() {
$csrf = Minz_Session::param('csrf');
if ($csrf == '') {
$salt = FreshRSS_Context::$system_conf->salt;
$csrf = sha1($salt . uniqid(mt_rand(), true));
Minz_Session::_param('csrf', $csrf);
}
return $csrf;
}
public static function isCsrfOk($token = null) {
$csrf = Minz_Session::param('csrf');
if ($csrf == '') {
return true; //Not logged in yet
}
if ($token === null) {
$token = Minz_Request::fetchPOST('_csrf');
}
return $token === $csrf;
}
}
class FreshRSS_FormAuth {
public static function checkCredentials($username, $hash, $nonce, $challenge) {
if (!ctype_alnum($username) ||
!ctype_graph($challenge) ||
!ctype_alnum($nonce)) {
Minz_Log::debug('Invalid credential parameters:' .
' user=' . $username .
' challenge=' . $challenge .
' nonce=' . $nonce);
return false;
}
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
return password_verify($nonce . $hash, $challenge);
}
public static function getCredentialsFromCookie() {
$token = Minz_Session::getLongTermCookie('FreshRSS_login');
if (!ctype_alnum($token)) {
return array();
}
$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
$mtime = @filemtime($token_file);
if ($mtime + 2629744 < time()) {
// Token has expired (> 1 month) or does not exist.
// TODO: 1 month -> use a configuration instead
@unlink($token_file);
return array();
}
$credentials = @file_get_contents($token_file);
return $credentials === false ? array() : explode("\t", $credentials, 2);
}
public static function makeCookie($username, $password_hash) {
$conf = Minz_Configuration::get('system');
do {
$token = sha1($conf->salt . $username . uniqid(mt_rand(), true));
$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
} while (file_exists($token_file));
if (@file_put_contents($token_file, $username . "\t" . $password_hash) === false) {
return false;
}
$limits = $conf->limits;
$cookie_duration = empty($limits['cookie_duration']) ? 2629744 : $limits['cookie_duration'];
$expire = time() + $cookie_duration;
Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
return $token;
}
public static function deleteCookie() {
$token = Minz_Session::getLongTermCookie('FreshRSS_login');
if (ctype_alnum($token)) {
Minz_Session::deleteLongTermCookie('FreshRSS_login');
@unlink(DATA_PATH . '/tokens/' . $token . '.txt');
}
if (rand(0, 10) === 1) {
self::purgeTokens();
}
}
public static function purgeTokens() {
$conf = Minz_Configuration::get('system');
$limits = $conf->limits;
$cookie_duration = empty($limits['cookie_duration']) ? 2629744 : $limits['cookie_duration'];
$oldest = time() - $cookie_duration;
foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {
// $extension = $file_info->getExtension(); doesn't work in PHP < 5.3.7
$extension = pathinfo($file_info->getFilename(), PATHINFO_EXTENSION);
if ($extension === 'txt' && $file_info->getMTime() < $oldest) {
@unlink($file_info->getPathname());
}
}
}
}

View File

@@ -6,66 +6,73 @@ class FreshRSS_Category extends Minz_Model {
private $nbFeed = -1;
private $nbNotRead = -1;
private $feeds = null;
private $hasFeedsWithError = false;
public function __construct ($name = '', $feeds = null) {
$this->_name ($name);
if (isset ($feeds)) {
$this->_feeds ($feeds);
public function __construct($name = '', $feeds = null) {
$this->_name($name);
if (isset($feeds)) {
$this->_feeds($feeds);
$this->nbFeed = 0;
$this->nbNotRead = 0;
foreach ($feeds as $feed) {
$this->nbFeed++;
$this->nbNotRead += $feed->nbNotRead ();
$this->nbNotRead += $feed->nbNotRead();
$this->hasFeedsWithError |= $feed->inError();
}
}
}
public function id () {
public function id() {
return $this->id;
}
public function name () {
public function name() {
return $this->name;
}
public function nbFeed () {
public function nbFeed() {
if ($this->nbFeed < 0) {
$catDAO = new FreshRSS_CategoryDAO ();
$this->nbFeed = $catDAO->countFeed ($this->id ());
$catDAO = new FreshRSS_CategoryDAO();
$this->nbFeed = $catDAO->countFeed($this->id());
}
return $this->nbFeed;
}
public function nbNotRead () {
public function nbNotRead() {
if ($this->nbNotRead < 0) {
$catDAO = new FreshRSS_CategoryDAO ();
$this->nbNotRead = $catDAO->countNotRead ($this->id ());
$catDAO = new FreshRSS_CategoryDAO();
$this->nbNotRead = $catDAO->countNotRead($this->id());
}
return $this->nbNotRead;
}
public function feeds () {
public function feeds() {
if ($this->feeds === null) {
$feedDAO = new FreshRSS_FeedDAO ();
$this->feeds = $feedDAO->listByCategory ($this->id ());
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->feeds = $feedDAO->listByCategory($this->id());
$this->nbFeed = 0;
$this->nbNotRead = 0;
foreach ($this->feeds as $feed) {
$this->nbFeed++;
$this->nbNotRead += $feed->nbNotRead ();
$this->nbNotRead += $feed->nbNotRead();
$this->hasFeedsWithError |= $feed->inError();
}
}
return $this->feeds;
}
public function _id ($value) {
public function hasFeedsWithError() {
return $this->hasFeedsWithError;
}
public function _id($value) {
$this->id = $value;
}
public function _name ($value) {
$this->name = $value;
public function _name($value) {
$this->name = substr(trim($value), 0, 255);
}
public function _feeds ($values) {
if (!is_array ($values)) {
$values = array ($values);
public function _feeds($values) {
if (!is_array($values)) {
$values = array($values);
}
$this->feeds = $values;

View File

@@ -1,171 +1,190 @@
<?php
class FreshRSS_CategoryDAO extends Minz_ModelPdo {
public function addCategory ($valuesTmp) {
$sql = 'INSERT INTO `' . $this->prefix . 'category` (name) VALUES(?)';
$stm = $this->bd->prepare ($sql);
class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$values = array (
const defaultCategoryId = 1;
public function addCategory($valuesTmp) {
$sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)';
$stm = $this->bd->prepare($sql);
$values = array(
substr($valuesTmp['name'], 0, 255),
);
if ($stm && $stm->execute ($values)) {
return $this->bd->lastInsertId();
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error addCategory: ' . $info[2]);
return false;
}
}
public function updateCategory ($id, $valuesTmp) {
$sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?';
$stm = $this->bd->prepare ($sql);
public function addCategoryObject($category) {
$cat = $this->searchByName($category->name());
if (!$cat) {
// Category does not exist yet in DB so we add it before continue
$values = array(
'name' => $category->name(),
);
return $this->addCategory($values);
}
$values = array (
return $cat->id();
}
public function updateCategory($id, $valuesTmp) {
$sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?';
$stm = $this->bd->prepare($sql);
$values = array(
$valuesTmp['name'],
$id
);
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error updateCategory: ' . $info[2]);
return false;
}
}
public function deleteCategory ($id) {
public function deleteCategory($id) {
if ($id <= self::defaultCategoryId) {
return false;
}
$sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($id);
$values = array($id);
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error deleteCategory: ' . $info[2]);
return false;
}
}
public function searchById ($id) {
public function searchById($id) {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($id);
$values = array($id);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$cat = self::daoToCategory ($res);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$cat = self::daoToCategory($res);
if (isset ($cat[0])) {
if (isset($cat[0])) {
return $cat[0];
} else {
return false;
return null;
}
}
public function searchByName ($name) {
public function searchByName($name) {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE name=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($name);
$values = array($name);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$cat = self::daoToCategory ($res);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$cat = self::daoToCategory($res);
if (isset ($cat[0])) {
if (isset($cat[0])) {
return $cat[0];
} else {
return false;
return null;
}
}
public function listCategories ($prePopulateFeeds = true, $details = false) {
public function listCategories($prePopulateFeeds = true, $details = false) {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ')
. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads` ')
. 'FROM `' . $this->prefix . 'category` c '
. 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category = c.id '
. 'GROUP BY f.id '
. 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
. 'GROUP BY f.id, c_id '
. 'ORDER BY c.name, f.name';
$stm = $this->bd->prepare ($sql);
$stm->execute ();
return self::daoToCategoryPrepopulated ($stm->fetchAll (PDO::FETCH_ASSOC));
$stm = $this->bd->prepare($sql);
$stm->execute();
return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` ORDER BY name';
$stm = $this->bd->prepare ($sql);
$stm->execute ();
return self::daoToCategory ($stm->fetchAll (PDO::FETCH_ASSOC));
$stm = $this->bd->prepare($sql);
$stm->execute();
return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
}
}
public function getDefault () {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=1';
$stm = $this->bd->prepare ($sql);
public function getDefault() {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=' . self::defaultCategoryId;
$stm = $this->bd->prepare($sql);
$stm->execute ();
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$cat = self::daoToCategory ($res);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$cat = self::daoToCategory($res);
if (isset ($cat[0])) {
if (isset($cat[0])) {
return $cat[0];
} else {
return false;
}
}
public function checkDefault () {
$def_cat = $this->searchById (1);
public function checkDefault() {
$def_cat = $this->searchById(self::defaultCategoryId);
if ($def_cat === false) {
$cat = new FreshRSS_Category (Minz_Translate::t ('default_category'));
$cat->_id (1);
if ($def_cat == null) {
$cat = new FreshRSS_Category(_t('gen.short.default_category'));
$cat->_id(self::defaultCategoryId);
$values = array (
'id' => $cat->id (),
'name' => $cat->name (),
$values = array(
'id' => $cat->id(),
'name' => $cat->name(),
);
$this->addCategory ($values);
$this->addCategory($values);
}
}
public function count () {
public function count() {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'category`';
$stm = $this->bd->prepare ($sql);
$stm->execute ();
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $res[0]['count'];
}
public function countFeed ($id) {
public function countFeed($id) {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'feed` WHERE category=?';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $res[0]['count'];
}
public function countNotRead ($id) {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE category=? AND e.is_read=0';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
public function countNotRead($id) {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE category=? AND e.is_read=0';
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $res[0]['count'];
}
public static function findFeed($categories, $feed_id) {
foreach ($categories as $category) {
foreach ($category->feeds () as $feed) {
if ($feed->id () === $feed_id) {
foreach ($category->feeds() as $feed) {
if ($feed->id() === $feed_id) {
return $feed;
}
}
@@ -176,8 +195,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
public static function CountUnreads($categories, $minPriority = 0) {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds () as $feed) {
if ($feed->priority () >= $minPriority) {
foreach ($category->feeds() as $feed) {
if ($feed->priority() >= $minPriority) {
$n += $feed->nbNotRead();
}
}
@@ -185,23 +204,24 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
return $n;
}
public static function daoToCategoryPrepopulated ($listDAO) {
$list = array ();
public static function daoToCategoryPrepopulated($listDAO) {
$list = array();
if (!is_array ($listDAO)) {
$listDAO = array ($listDAO);
if (!is_array($listDAO)) {
$listDAO = array($listDAO);
}
$previousLine = null;
$feedsDao = array();
$feedDao = FreshRSS_Factory::createFeedDAO();
foreach ($listDAO as $line) {
if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
// End of the current category, we add it to the $list
$cat = new FreshRSS_Category (
$cat = new FreshRSS_Category(
$previousLine['c_name'],
FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id'])
$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
);
$cat->_id ($previousLine['c_id']);
$cat->_id($previousLine['c_id']);
$list[$previousLine['c_id']] = $cat;
$feedsDao = array(); //Prepare for next category
@@ -213,29 +233,29 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
// add the last category
if ($previousLine != null) {
$cat = new FreshRSS_Category (
$cat = new FreshRSS_Category(
$previousLine['c_name'],
FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id'])
$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
);
$cat->_id ($previousLine['c_id']);
$cat->_id($previousLine['c_id']);
$list[$previousLine['c_id']] = $cat;
}
return $list;
}
public static function daoToCategory ($listDAO) {
$list = array ();
public static function daoToCategory($listDAO) {
$list = array();
if (!is_array ($listDAO)) {
$listDAO = array ($listDAO);
if (!is_array($listDAO)) {
$listDAO = array($listDAO);
}
foreach ($listDAO as $key => $dao) {
$cat = new FreshRSS_Category (
$cat = new FreshRSS_Category(
$dao['name']
);
$cat->_id ($dao['id']);
$cat->_id($dao['id']);
$list[$key] = $cat;
}

View File

@@ -1,249 +0,0 @@
<?php
class FreshRSS_Configuration {
private $filename;
private $data = array(
'language' => 'en',
'old_entries' => 3,
'keep_history_default' => 0,
'mail_login' => '',
'token' => '',
'passwordHash' => '', //CRYPT_BLOWFISH
'posts_per_page' => 20,
'view_mode' => 'normal',
'default_view' => 'not_read',
'auto_load_more' => true,
'display_posts' => false,
'onread_jump_next' => true,
'lazyload' => true,
'sort_order' => 'DESC',
'anon_access' => false,
'mark_when' => array(
'article' => true,
'site' => true,
'scroll' => false,
'reception' => false,
),
'theme' => 'Origine',
'shortcuts' => array(
'mark_read' => 'r',
'mark_favorite' => 'f',
'go_website' => 'space',
'next_entry' => 'j',
'prev_entry' => 'k',
'first_entry' => 'home',
'last_entry' => 'end',
'collapse_entry' => 'c',
'load_more' => 'm',
'auto_share' => 's',
),
'topline_read' => true,
'topline_favorite' => true,
'topline_date' => true,
'topline_link' => true,
'bottomline_read' => true,
'bottomline_favorite' => true,
'bottomline_sharing' => true,
'bottomline_tags' => true,
'bottomline_date' => true,
'bottomline_link' => true,
'sharing' => array(
'shaarli' => '',
'wallabag' => '',
'diaspora' => '',
'twitter' => true,
'g+' => true,
'facebook' => true,
'email' => true,
'print' => true,
),
);
private $available_languages = array(
'en' => 'English',
'fr' => 'Français',
);
public function __construct ($user) {
$this->filename = DATA_PATH . '/' . $user . '_user.php';
$data = @include($this->filename);
if (!is_array($data)) {
throw new Minz_PermissionDeniedException($this->filename);
}
foreach ($data as $key => $value) {
if (isset($this->data[$key])) {
$function = '_' . $key;
$this->$function($value);
}
}
$this->data['user'] = $user;
}
public function save() {
@rename($this->filename, $this->filename . '.bak.php');
if (file_put_contents($this->filename, "<?php\n return " . var_export($this->data, true) . ';', LOCK_EX) === false) {
throw new Minz_PermissionDeniedException($this->filename);
}
if (function_exists('opcache_invalidate')) {
opcache_invalidate($this->filename); //Clear PHP 5.5+ cache for include
}
invalidateHttpCache();
return true;
}
public function __get($name) {
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
} else {
$trace = debug_backtrace();
trigger_error('Undefined FreshRSS_Configuration->' . $name . 'in ' . $trace[0]['file'] . ' line ' . $trace[0]['line'], E_USER_NOTICE); //TODO: Use Minz exceptions
return null;
}
}
public function sharing($key = false) {
if ($key === false) {
return $this->data['sharing'];
}
if (isset($this->data['sharing'][$key])) {
return $this->data['sharing'][$key];
}
return false;
}
public function availableLanguages() {
return $this->available_languages;
}
public function _language($value) {
if (!isset($this->available_languages[$value])) {
$value = 'en';
}
$this->data['language'] = $value;
}
public function _posts_per_page ($value) {
$value = intval($value);
$this->data['posts_per_page'] = $value > 0 ? $value : 10;
}
public function _view_mode ($value) {
if ($value === 'global' || $value === 'reader') {
$this->data['view_mode'] = $value;
} else {
$this->data['view_mode'] = 'normal';
}
}
public function _default_view ($value) {
$this->data['default_view'] = $value === 'all' ? 'all' : 'not_read';
}
public function _display_posts ($value) {
$this->data['display_posts'] = ((bool)$value) && $value !== 'no';
}
public function _onread_jump_next ($value) {
$this->data['onread_jump_next'] = ((bool)$value) && $value !== 'no';
}
public function _lazyload ($value) {
$this->data['lazyload'] = ((bool)$value) && $value !== 'no';
}
public function _sort_order ($value) {
$this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC';
}
public function _old_entries($value) {
$value = intval($value);
$this->data['old_entries'] = $value > 0 ? $value : 3;
}
public function _keep_history_default($value) {
$value = intval($value);
$this->data['keep_history_default'] = $value >= -1 ? $value : 0;
}
public function _shortcuts ($values) {
foreach ($values as $key => $value) {
if (isset($this->data['shortcuts'][$key])) {
$this->data['shortcuts'][$key] = $value;
}
}
}
public function _passwordHash ($value) {
$this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
public function _mail_login ($value) {
$value = filter_var($value, FILTER_VALIDATE_EMAIL);
if ($value) {
$this->data['mail_login'] = $value;
} else {
$this->data['mail_login'] = '';
}
}
public function _anon_access ($value) {
$this->data['anon_access'] = ((bool)$value) && $value !== 'no';
}
public function _mark_when ($values) {
foreach ($values as $key => $value) {
if (isset($this->data['mark_when'][$key])) {
$this->data['mark_when'][$key] = ((bool)$value) && $value !== 'no';
}
}
}
public function _sharing ($values) {
$are_url = array ('shaarli', 'wallabag', 'diaspora');
foreach ($values as $key => $value) {
if (in_array($key, $are_url)) {
$is_url = (
filter_var ($value, FILTER_VALIDATE_URL) ||
(version_compare(PHP_VERSION, '5.3.3', '<') &&
(strpos($value, '-') > 0) &&
($value === filter_var($value, FILTER_SANITIZE_URL)))
); //PHP bug #51192
if (!$is_url) {
$value = '';
}
} elseif (!is_bool($value)) {
$value = true;
}
$this->data['sharing'][$key] = $value;
}
}
public function _theme($value) {
$this->data['theme'] = $value;
}
public function _token($value) {
$this->data['token'] = $value;
}
public function _auto_load_more($value) {
$this->data['auto_load_more'] = ((bool)$value) && $value !== 'no';
}
public function _topline_read($value) {
$this->data['topline_read'] = ((bool)$value) && $value !== 'no';
}
public function _topline_favorite($value) {
$this->data['topline_favorite'] = ((bool)$value) && $value !== 'no';
}
public function _topline_date($value) {
$this->data['topline_date'] = ((bool)$value) && $value !== 'no';
}
public function _topline_link($value) {
$this->data['topline_link'] = ((bool)$value) && $value !== 'no';
}
public function _bottomline_read($value) {
$this->data['bottomline_read'] = ((bool)$value) && $value !== 'no';
}
public function _bottomline_favorite($value) {
$this->data['bottomline_favorite'] = ((bool)$value) && $value !== 'no';
}
public function _bottomline_sharing($value) {
$this->data['bottomline_sharing'] = ((bool)$value) && $value !== 'no';
}
public function _bottomline_tags($value) {
$this->data['bottomline_tags'] = ((bool)$value) && $value !== 'no';
}
public function _bottomline_date($value) {
$this->data['bottomline_date'] = ((bool)$value) && $value !== 'no';
}
public function _bottomline_link($value) {
$this->data['bottomline_link'] = ((bool)$value) && $value !== 'no';
}
}

View File

@@ -0,0 +1,380 @@
<?php
class FreshRSS_ConfigurationSetter {
/**
* Return if the given key is supported by this setter.
* @param $key the key to test.
* @return true if the key is supported, false else.
*/
public function support($key) {
$name_setter = '_' . $key;
return is_callable(array($this, $name_setter));
}
/**
* Set the given key in data with the current value.
* @param $data an array containing the list of all configuration data.
* @param $key the key to update.
* @param $value the value to set.
*/
public function handle(&$data, $key, $value) {
$name_setter = '_' . $key;
call_user_func_array(array($this, $name_setter), array(&$data, $value));
}
/**
* A helper to set boolean values.
*
* @param $value the tested value.
* @return true if value is true and different from no, false else.
*/
private function handleBool($value) {
return ((bool)$value) && $value !== 'no';
}
/**
* The (long) list of setters for user configuration.
*/
private function _apiPasswordHash(&$data, $value) {
$data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
private function _content_width(&$data, $value) {
$value = strtolower($value);
if (!in_array($value, array('thin', 'medium', 'large', 'no_limit'))) {
$value = 'thin';
}
$data['content_width'] = $value;
}
private function _default_state(&$data, $value) {
$data['default_state'] = (int)$value;
}
private function _default_view(&$data, $value) {
switch ($value) {
case 'all':
$data['default_view'] = $value;
$data['default_state'] = (FreshRSS_Entry::STATE_READ +
FreshRSS_Entry::STATE_NOT_READ);
break;
case 'adaptive':
case 'unread':
default:
$data['default_view'] = $value;
$data['default_state'] = FreshRSS_Entry::STATE_NOT_READ;
}
}
// It works for system config too!
private function _extensions_enabled(&$data, $value) {
if (!is_array($value)) {
$value = array($value);
}
$data['extensions_enabled'] = $value;
}
private function _html5_notif_timeout(&$data, $value) {
$value = intval($value);
$data['html5_notif_timeout'] = $value >= 0 ? $value : 0;
}
private function _keep_history_default(&$data, $value) {
$value = intval($value);
$data['keep_history_default'] = $value >= -1 ? $value : 0;
}
// It works for system config too!
private function _language(&$data, $value) {
$value = strtolower($value);
$languages = Minz_Translate::availableLanguages();
if (!in_array($value, $languages)) {
$value = 'en';
}
$data['language'] = $value;
}
private function _old_entries(&$data, $value) {
$value = intval($value);
$data['old_entries'] = $value > 0 ? $value : 3;
}
private function _passwordHash(&$data, $value) {
$data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
private function _posts_per_page(&$data, $value) {
$value = intval($value);
$data['posts_per_page'] = $value > 0 ? $value : 10;
}
private function _queries(&$data, $values) {
$data['queries'] = array();
foreach ($values as $value) {
if ($value instanceof FreshRSS_UserQuery) {
$data['queries'][] = $value->toArray();
} elseif (is_array($value)) {
$data['queries'][] = $value;
}
}
}
private function _sharing(&$data, $values) {
$data['sharing'] = array();
foreach ($values as $value) {
if (!is_array($value)) {
continue;
}
// Verify URL and add default value when needed
if (isset($value['url'])) {
$is_url = filter_var($value['url'], FILTER_VALIDATE_URL);
if (!$is_url) {
continue;
}
} else {
$value['url'] = null;
}
$data['sharing'][] = $value;
}
}
private function _shortcuts(&$data, $values) {
if (!is_array($values)) {
return;
}
$data['shortcuts'] = $values;
}
private function _sort_order(&$data, $value) {
$data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC';
}
private function _ttl_default(&$data, $value) {
$value = intval($value);
$data['ttl_default'] = $value >= -1 ? $value : 3600;
}
private function _view_mode(&$data, $value) {
$value = strtolower($value);
if (!in_array($value, array('global', 'normal', 'reader'))) {
$value = 'normal';
}
$data['view_mode'] = $value;
}
/**
* A list of boolean setters.
*/
private function _anon_access(&$data, $value) {
$data['anon_access'] = $this->handleBool($value);
}
private function _auto_load_more(&$data, $value) {
$data['auto_load_more'] = $this->handleBool($value);
}
private function _auto_remove_article(&$data, $value) {
$data['auto_remove_article'] = $this->handleBool($value);
}
private function _mark_updated_article_unread(&$data, $value) {
$data['mark_updated_article_unread'] = $this->handleBool($value);
}
private function _display_categories(&$data, $value) {
$data['display_categories'] = $this->handleBool($value);
}
private function _display_posts(&$data, $value) {
$data['display_posts'] = $this->handleBool($value);
}
private function _hide_read_feeds(&$data, $value) {
$data['hide_read_feeds'] = $this->handleBool($value);
}
private function _lazyload(&$data, $value) {
$data['lazyload'] = $this->handleBool($value);
}
private function _mark_when(&$data, $values) {
foreach ($values as $key => $value) {
$data['mark_when'][$key] = $this->handleBool($value);
}
}
private function _onread_jump_next(&$data, $value) {
$data['onread_jump_next'] = $this->handleBool($value);
}
private function _reading_confirm(&$data, $value) {
$data['reading_confirm'] = $this->handleBool($value);
}
private function _sticky_post(&$data, $value) {
$data['sticky_post'] = $this->handleBool($value);
}
private function _bottomline_date(&$data, $value) {
$data['bottomline_date'] = $this->handleBool($value);
}
private function _bottomline_favorite(&$data, $value) {
$data['bottomline_favorite'] = $this->handleBool($value);
}
private function _bottomline_link(&$data, $value) {
$data['bottomline_link'] = $this->handleBool($value);
}
private function _bottomline_read(&$data, $value) {
$data['bottomline_read'] = $this->handleBool($value);
}
private function _bottomline_sharing(&$data, $value) {
$data['bottomline_sharing'] = $this->handleBool($value);
}
private function _bottomline_tags(&$data, $value) {
$data['bottomline_tags'] = $this->handleBool($value);
}
private function _topline_date(&$data, $value) {
$data['topline_date'] = $this->handleBool($value);
}
private function _topline_favorite(&$data, $value) {
$data['topline_favorite'] = $this->handleBool($value);
}
private function _topline_link(&$data, $value) {
$data['topline_link'] = $this->handleBool($value);
}
private function _topline_read(&$data, $value) {
$data['topline_read'] = $this->handleBool($value);
}
/**
* The (not so long) list of setters for system configuration.
*/
private function _allow_anonymous(&$data, $value) {
$data['allow_anonymous'] = $this->handleBool($value) && FreshRSS_Auth::accessNeedsAction();
}
private function _allow_anonymous_refresh(&$data, $value) {
$data['allow_anonymous_refresh'] = $this->handleBool($value) && $data['allow_anonymous'];
}
private function _api_enabled(&$data, $value) {
$data['api_enabled'] = $this->handleBool($value);
}
private function _auth_type(&$data, $value) {
$value = strtolower($value);
if (!in_array($value, array('form', 'http_auth', 'none'))) {
$value = 'none';
}
$data['auth_type'] = $value;
$this->_allow_anonymous($data, $data['allow_anonymous']);
}
private function _db(&$data, $value) {
if (!isset($value['type'])) {
return;
}
switch ($value['type']) {
case 'mysql':
case 'pgsql':
if (empty($value['host']) ||
empty($value['user']) ||
empty($value['base']) ||
!isset($value['password'])) {
return;
}
$data['db']['type'] = $value['type'];
$data['db']['host'] = $value['host'];
$data['db']['user'] = $value['user'];
$data['db']['base'] = $value['base'];
$data['db']['password'] = $value['password'];
$data['db']['prefix'] = isset($value['prefix']) ? $value['prefix'] : '';
break;
case 'sqlite':
$data['db']['type'] = $value['type'];
$data['db']['host'] = '';
$data['db']['user'] = '';
$data['db']['base'] = '';
$data['db']['password'] = '';
$data['db']['prefix'] = '';
break;
default:
return;
}
}
private function _default_user(&$data, $value) {
$user_list = listUsers();
if (in_array($value, $user_list)) {
$data['default_user'] = $value;
}
}
private function _environment(&$data, $value) {
$value = strtolower($value);
if (!in_array($value, array('silent', 'development', 'production'))) {
$value = 'production';
}
$data['environment'] = $value;
}
private function _limits(&$data, $values) {
$max_small_int = 16384;
$limits_keys = array(
'cache_duration' => array(
'min' => 0,
),
'timeout' => array(
'min' => 0,
),
'max_inactivity' => array(
'min' => 0,
),
'max_feeds' => array(
'min' => 0,
'max' => $max_small_int,
),
'max_categories' => array(
'min' => 0,
'max' => $max_small_int,
),
'max_registrations' => array(
'min' => 0,
),
);
foreach ($values as $key => $value) {
if (!isset($limits_keys[$key])) {
continue;
}
$value = intval($value);
$limits = $limits_keys[$key];
if (
(!isset($limits['min']) || $value >= $limits['min']) &&
(!isset($limits['max']) || $value <= $limits['max'])
) {
$data['limits'][$key] = $value;
}
}
}
private function _unsafe_autologin_enabled(&$data, $value) {
$data['unsafe_autologin_enabled'] = $this->handleBool($value);
}
private function _auto_update_url(&$data, $value) {
if (!$value) {
return;
}
$data['auto_update_url'] = $value;
}
}

326
app/Models/Context.php Normal file
View File

@@ -0,0 +1,326 @@
<?php
/**
* The context object handles the current configuration file and different
* useful functions associated to the current view state.
*/
class FreshRSS_Context {
public static $user_conf = null;
public static $system_conf = null;
public static $categories = array();
public static $name = '';
public static $description = '';
public static $total_unread = 0;
public static $total_starred = array(
'all' => 0,
'read' => 0,
'unread' => 0,
);
public static $get_unread = 0;
public static $current_get = array(
'all' => false,
'starred' => false,
'feed' => false,
'category' => false,
);
public static $next_get = 'a';
public static $state = 0;
public static $order = 'DESC';
public static $number = 0;
public static $search;
public static $first_id = '';
public static $next_id = '';
public static $id_max = '';
public static $sinceHours = 0;
public static $isCli = false;
/**
* Initialize the context.
*
* Set the correct configurations and $categories variables.
*/
public static function init() {
// Init configuration.
self::$system_conf = Minz_Configuration::get('system');
self::$user_conf = Minz_Configuration::get('user');
}
/**
* Returns if the current state includes $state parameter.
*/
public static function isStateEnabled($state) {
return self::$state & $state;
}
/**
* Returns the current state with or without $state parameter.
*/
public static function getRevertState($state) {
if (self::$state & $state) {
return self::$state & ~$state;
} else {
return self::$state | $state;
}
}
/**
* Return the current get as a string or an array.
*
* If $array is true, the first item of the returned value is 'f' or 'c' and
* the second is the id.
*/
public static function currentGet($array = false) {
if (self::$current_get['all']) {
return 'a';
} elseif (self::$current_get['starred']) {
return 's';
} elseif (self::$current_get['feed']) {
if ($array) {
return array('f', self::$current_get['feed']);
} else {
return 'f_' . self::$current_get['feed'];
}
} elseif (self::$current_get['category']) {
if ($array) {
return array('c', self::$current_get['category']);
} else {
return 'c_' . self::$current_get['category'];
}
}
}
/**
* Return true if the current request targets a feed (and not a category or all articles), false otherwise.
*/
public static function isFeed() {
return self::$current_get['feed'] != false;
}
/**
* Return true if $get parameter correspond to the $current_get attribute.
*/
public static function isCurrentGet($get) {
$type = $get[0];
$id = substr($get, 2);
switch($type) {
case 'a':
return self::$current_get['all'];
case 's':
return self::$current_get['starred'];
case 'f':
return self::$current_get['feed'] == $id;
case 'c':
return self::$current_get['category'] == $id;
default:
return false;
}
}
/**
* Set the current $get attribute.
*
* Valid $get parameter are:
* - a
* - s
* - f_<feed id>
* - c_<category id>
*
* $name and $get_unread attributes are also updated as $next_get
* Raise an exception if id or $get is invalid.
*/
public static function _get($get) {
$type = $get[0];
$id = substr($get, 2);
$nb_unread = 0;
if (empty(self::$categories)) {
$catDAO = new FreshRSS_CategoryDAO();
self::$categories = $catDAO->listCategories();
}
switch($type) {
case 'a':
self::$current_get['all'] = true;
self::$name = _t('index.feed.title');
self::$description = self::$system_conf->meta_description;
self::$get_unread = self::$total_unread;
break;
case 's':
self::$current_get['starred'] = true;
self::$name = _t('index.feed.title_fav');
self::$description = self::$system_conf->meta_description;
self::$get_unread = self::$total_starred['unread'];
// Update state if favorite is not yet enabled.
self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE;
break;
case 'f':
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
$feed = FreshRSS_Context::$system_conf->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
if (!$feed) {
throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
}
}
self::$current_get['feed'] = $id;
self::$current_get['category'] = $feed->category();
self::$name = $feed->name();
self::$description = $feed->description();
self::$get_unread = $feed->nbNotRead();
break;
case 'c':
// We try to find the corresponding category.
self::$current_get['category'] = $id;
if (!isset(self::$categories[$id])) {
$catDAO = new FreshRSS_CategoryDAO();
$cat = $catDAO->searchById($id);
if (!$cat) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
} else {
$cat = self::$categories[$id];
}
self::$name = $cat->name();
self::$get_unread = $cat->nbNotRead();
break;
default:
throw new FreshRSS_Context_Exception('Invalid getter: ' . $get);
}
self::_nextGet();
}
/**
* Set the value of $next_get attribute.
*/
private static function _nextGet() {
$get = self::currentGet();
// By default, $next_get == $get
self::$next_get = $get;
if (empty(self::$categories)) {
$catDAO = new FreshRSS_CategoryDAO();
self::$categories = $catDAO->listCategories();
}
if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
$another_unread_id = '';
$found_current_get = false;
switch ($get[0]) {
case 'f':
// We search the next feed with at least one unread article in
// same category as the currend feed.
foreach (self::$categories as $cat) {
if ($cat->id() != self::$current_get['category']) {
// We look into the category of the current feed!
continue;
}
foreach ($cat->feeds() as $feed) {
if ($feed->id() == self::$current_get['feed']) {
// Here is our current feed! Fine, the next one will
// be a potential candidate.
$found_current_get = true;
continue;
}
if ($feed->nbNotRead() > 0) {
$another_unread_id = $feed->id();
if ($found_current_get) {
// We have found our current feed and now we
// have an feed with unread articles. Leave the
// loop!
break;
}
}
}
break;
}
// If no feed have been found, next_get is the current category.
self::$next_get = empty($another_unread_id) ?
'c_' . self::$current_get['category'] :
'f_' . $another_unread_id;
break;
case 'c':
// We search the next category with at least one unread article.
foreach (self::$categories as $cat) {
if ($cat->id() == self::$current_get['category']) {
// Here is our current category! Next one could be our
// champion if it has unread articles.
$found_current_get = true;
continue;
}
if ($cat->nbNotRead() > 0) {
$another_unread_id = $cat->id();
if ($found_current_get) {
// Unread articles and the current category has
// already been found? Leave the loop!
break;
}
}
}
// No unread category? The main stream will be our destination!
self::$next_get = empty($another_unread_id) ?
'a' :
'c_' . $another_unread_id;
break;
}
}
}
/**
* Determine if the auto remove is available in the current context.
* This feature is available if:
* - it is activated in the configuration
* - the "read" state is not enable
* - the "unread" state is enable
*
* @return boolean
*/
public static function isAutoRemoveAvailable() {
if (!self::$user_conf->auto_remove_article) {
return false;
}
if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
return false;
}
if (!self::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ)) {
return false;
}
return true;
}
/**
* Determine if the "sticky post" option is enabled. It can be enable
* by the user when it is selected in the configuration page or by the
* application when the context allows to auto-remove articles when they
* are read.
*
* @return boolean
*/
public static function isStickyPostEnabled() {
if (self::$user_conf->sticky_post) {
return true;
}
if (self::isAutoRemoveAvailable()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* This class is used to test database is well-constructed.
*/
class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
public function tablesAreCorrect() {
$sql = 'SHOW TABLES';
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$tables = array(
$this->prefix . 'category' => false,
$this->prefix . 'feed' => false,
$this->prefix . 'entry' => false,
);
foreach ($res as $value) {
$tables[array_pop($value)] = true;
}
return count(array_keys($tables, true, true)) == count($tables);
}
public function getSchema($table) {
$sql = 'DESC ' . $this->prefix . $table;
$stm = $this->bd->prepare($sql);
$stm->execute();
return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
}
public function checkTable($table, $schema) {
$columns = $this->getSchema($table);
$ok = (count($columns) == count($schema));
foreach ($columns as $c) {
$ok &= in_array($c['name'], $schema);
}
return $ok;
}
public function categoryIsCorrect() {
return $this->checkTable('category', array(
'id', 'name'
));
}
public function feedIsCorrect() {
return $this->checkTable('feed', array(
'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl',
'cache_nbEntries', 'cache_nbUnreads'
));
}
public function entryIsCorrect() {
return $this->checkTable('entry', array(
'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'is_read',
'is_favorite', 'id_feed', 'tags'
));
}
public function daoToSchema($dao) {
return array(
'name' => $dao['Field'],
'type' => strtolower($dao['Type']),
'notnull' => (bool)$dao['Null'],
'default' => $dao['Default'],
);
}
public function listDaoToSchema($listDAO) {
$list = array();
foreach ($listDAO as $dao) {
$list[] = $this->daoToSchema($dao);
}
return $list;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* This class is used to test database is well-constructed.
*/
class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
public function tablesAreCorrect() {
$db = FreshRSS_Context::$system_conf->db;
$dbowner = $db['user'];
$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
$stm = $this->bd->prepare($sql);
$values = array($dbowner);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$tables = array(
$this->prefix . 'category' => false,
$this->prefix . 'feed' => false,
$this->prefix . 'entry' => false,
);
foreach ($res as $value) {
$tables[array_pop($value)] = true;
}
return count(array_keys($tables, true, true)) == count($tables);
}
public function getSchema($table) {
$sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?';
$stm = $this->bd->prepare($sql);
$stm->execute(array($this->prefix . $table));
return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
}
public function daoToSchema($dao) {
return array(
'name' => $dao['field'],
'type' => strtolower($dao['type']),
'notnull' => (bool)$dao['null'],
'default' => $dao['default'],
);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* This class is used to test database is well-constructed (SQLite).
*/
class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
public function tablesAreCorrect() {
$sql = 'SELECT name FROM sqlite_master WHERE type="table"';
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$tables = array(
'category' => false,
'feed' => false,
'entry' => false,
);
foreach ($res as $value) {
$tables[$value['name']] = true;
}
return count(array_keys($tables, true, true)) == count($tables);
}
public function getSchema($table) {
$sql = 'PRAGMA table_info(' . $table . ')';
$stm = $this->bd->prepare($sql);
$stm->execute();
return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
}
public function entryIsCorrect() {
return $this->checkTable('entry', array(
'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'is_read',
'is_favorite', 'id_feed', 'tags'
));
}
public function daoToSchema($dao) {
return array(
'name' => $dao['name'],
'type' => strtolower($dao['type']),
'notnull' => $dao['notnull'] === '1' ? true : false,
'default' => $dao['dflt_value'],
);
}
}

View File

@@ -1,6 +1,11 @@
<?php
class FreshRSS_Entry extends Minz_Model {
const STATE_READ = 1;
const STATE_NOT_READ = 2;
const STATE_ALL = 3;
const STATE_FAVORITE = 4;
const STATE_NOT_FAVORITE = 8;
private $id = 0;
private $guid;
@@ -9,139 +14,154 @@ class FreshRSS_Entry extends Minz_Model {
private $content;
private $link;
private $date;
private $is_read;
private $hash = null;
private $is_read; //Nullable boolean
private $is_favorite;
private $feed;
private $tags;
public function __construct ($feed = '', $guid = '', $title = '', $author = '', $content = '',
$link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
$this->_guid ($guid);
$this->_title ($title);
$this->_author ($author);
$this->_content ($content);
$this->_link ($link);
$this->_date ($pubdate);
$this->_isRead ($is_read);
$this->_isFavorite ($is_favorite);
$this->_feed ($feed);
$this->_tags (preg_split('/[\s#]/', $tags));
public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '',
$link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
$this->_guid($guid);
$this->_title($title);
$this->_author($author);
$this->_content($content);
$this->_link($link);
$this->_date($pubdate);
$this->_isRead($is_read);
$this->_isFavorite($is_favorite);
$this->_feed($feed);
$this->_tags(preg_split('/[\s#]/', $tags));
}
public function id () {
public function id() {
return $this->id;
}
public function guid () {
public function guid() {
return $this->guid;
}
public function title () {
public function title() {
return $this->title;
}
public function author () {
public function author() {
return $this->author === null ? '' : $this->author;
}
public function content () {
public function content() {
return $this->content;
}
public function link () {
public function link() {
return $this->link;
}
public function date ($raw = false) {
public function date($raw = false) {
if ($raw) {
return $this->date;
} else {
return timestamptodate ($this->date);
return timestamptodate($this->date);
}
}
public function dateAdded ($raw = false) {
public function dateAdded($raw = false) {
$date = intval(substr($this->id, 0, -6));
if ($raw) {
return $date;
} else {
return timestamptodate ($date);
return timestamptodate($date);
}
}
public function isRead () {
public function isRead() {
return $this->is_read;
}
public function isFavorite () {
public function isFavorite() {
return $this->is_favorite;
}
public function feed ($object = false) {
public function feed($object = false) {
if ($object) {
$feedDAO = new FreshRSS_FeedDAO ();
return $feedDAO->searchById ($this->feed);
$feedDAO = FreshRSS_Factory::createFeedDao();
return $feedDAO->searchById($this->feed);
} else {
return $this->feed;
}
}
public function tags ($inString = false) {
public function tags($inString = false) {
if ($inString) {
return empty ($this->tags) ? '' : '#' . implode(' #', $this->tags);
return empty($this->tags) ? '' : '#' . implode(' #', $this->tags);
} else {
return $this->tags;
}
}
public function _id ($value) {
public function hash() {
if ($this->hash === null) {
//Do not include $this->date because it may be automatically generated when lacking
$this->hash = md5($this->link . $this->title . $this->author . $this->content . $this->tags(true));
}
return $this->hash;
}
public function _id($value) {
$this->id = $value;
}
public function _guid ($value) {
public function _guid($value) {
$this->guid = $value;
}
public function _title ($value) {
public function _title($value) {
$this->hash = null;
$this->title = $value;
}
public function _author ($value) {
public function _author($value) {
$this->hash = null;
$this->author = $value;
}
public function _content ($value) {
public function _content($value) {
$this->hash = null;
$this->content = $value;
}
public function _link ($value) {
public function _link($value) {
$this->hash = null;
$this->link = $value;
}
public function _date ($value) {
public function _date($value) {
$this->hash = null;
$value = intval($value);
$this->date = $value > 1 ? $value : time();
}
public function _isRead ($value) {
$this->is_read = $value;
public function _isRead($value) {
$this->is_read = $value === null ? null : (bool)$value;
}
public function _isFavorite ($value) {
public function _isFavorite($value) {
$this->is_favorite = $value;
}
public function _feed ($value) {
public function _feed($value) {
$this->feed = $value;
}
public function _tags ($value) {
if (!is_array ($value)) {
$value = array ($value);
public function _tags($value) {
$this->hash = null;
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $key => $t) {
if (!$t) {
unset ($value[$key]);
unset($value[$key]);
}
}
$this->tags = $value;
}
public function isDay ($day, $today) {
public function isDay($day, $today) {
$date = $this->dateAdded(true);
switch ($day) {
case FreshRSS_Days::TODAY:
$tomorrow = $today + 86400;
return $date >= $today && $date < $tomorrow;
case FreshRSS_Days::YESTERDAY:
$yesterday = $today - 86400;
return $date >= $yesterday && $date < $today;
case FreshRSS_Days::BEFORE_YESTERDAY:
$yesterday = $today - 86400;
return $date < $yesterday;
default:
return false;
case FreshRSS_Days::TODAY:
$tomorrow = $today + 86400;
return $date >= $today && $date < $tomorrow;
case FreshRSS_Days::YESTERDAY:
$yesterday = $today - 86400;
return $date >= $yesterday && $date < $today;
case FreshRSS_Days::BEFORE_YESTERDAY:
$yesterday = $today - 86400;
return $date < $yesterday;
default:
return false;
}
}
@@ -149,10 +169,10 @@ class FreshRSS_Entry extends Minz_Model {
// Gestion du contenu
// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
if ($pathEntries) {
$entryDAO = new FreshRSS_EntryDAO();
$entryDAO = FreshRSS_Factory::createEntryDao();
$entry = $entryDAO->searchByGuid($this->feed, $this->guid);
if($entry) {
if ($entry) {
// l'article existe déjà en BDD, en se contente de recharger ce contenu
$this->content = $entry->content();
} else {
@@ -162,25 +182,26 @@ class FreshRSS_Entry extends Minz_Model {
htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries
);
} catch (Exception $e) {
// rien à faire, on garde l'ancien contenu (requête a échoué)
// rien à faire, on garde l'ancien contenu(requête a échoué)
}
}
}
}
public function toArray () {
return array (
'id' => $this->id (),
'guid' => $this->guid (),
'title' => $this->title (),
'author' => $this->author (),
'content' => $this->content (),
'link' => $this->link (),
'date' => $this->date (true),
'is_read' => $this->isRead (),
'is_favorite' => $this->isFavorite (),
'id_feed' => $this->feed (),
'tags' => $this->tags (true),
public function toArray() {
return array(
'id' => $this->id(),
'guid' => $this->guid(),
'title' => $this->title(),
'author' => $this->author(),
'content' => $this->content(),
'link' => $this->link(),
'date' => $this->date(true),
'hash' => $this->hash(),
'is_read' => $this->isRead(),
'is_favorite' => $this->isFavorite(),
'id_feed' => $this->feed(),
'tags' => $this->tags(true),
);
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
<?php
class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
public function sqlHexDecode($x) {
return 'decode(' . $x . ", 'hex')";
}
public function sqlHexEncode($x) {
return 'encode(' . $x . ", 'hex')";
}
protected function autoUpdateDb($errorInfo) {
return false;
}
protected function addColumn($name) {
return false;
}
public function size($all = true) {
$db = FreshRSS_Context::$system_conf->db;
$sql = 'SELECT pg_size_pretty(pg_database_size(?))';
$values = array($db['base']);
$stm = $this->bd->prepare($sql);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $res[0];
}
}

View File

@@ -0,0 +1,204 @@
<?php
class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
public function sqlHexDecode($x) {
return $x;
}
protected function autoUpdateDb($errorInfo) {
if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR
//autoAddColumn
if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
$showCreate = $tableInfo->fetchColumn();
Minz_Log::debug('FreshRSS_EntryDAOSQLite::autoUpdateDb: ' . $showCreate);
foreach (array('lastSeen', 'hash') as $column) {
if (stripos($showCreate, $column) === false) {
return $this->addColumn($column);
}
}
}
}
return false;
}
protected function sqlConcat($s1, $s2) {
return $s1 . '||' . $s2;
}
protected function updateCacheUnreads($catId = false, $feedId = false) {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET `cache_nbUnreads`=('
. 'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
. 'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0)';
$hasWhere = false;
$values = array();
if ($feedId !== false) {
$sql .= $hasWhere ? ' AND' : ' WHERE';
$hasWhere = true;
$sql .= ' id=?';
$values[] = $feedId;
}
if ($catId !== false) {
$sql .= $hasWhere ? ' AND' : ' WHERE';
$hasWhere = true;
$sql .= ' category=?';
$values[] = $catId;
}
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute($values)) {
return true;
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]);
return false;
}
}
/**
* Toggle the read marker on one or more article.
* Then the cache is updated.
*
* @todo change the way the query is build because it seems there is
* unnecessary code in here. For instance, the part with the str_repeat.
* @todo remove code duplication. It seems the code is basically the
* same if it is an array or not.
*
* @param integer|array $ids
* @param boolean $is_read
* @return integer affected rows
*/
public function markRead($ids, $is_read = true) {
if (is_array($ids)) { //Many IDs at once (used by API)
if (true) { //Speed heuristics //TODO: Not implemented yet for SQLite (so always call IDs one by one)
$affected = 0;
foreach ($ids as $id) {
$affected += $this->markRead($id, $is_read);
}
return $affected;
}
} else {
$this->bd->beginTransaction();
$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=? WHERE id=? AND is_read=?';
$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error markRead 1: ' . $info[2]);
$this->bd->rollBack();
return false;
}
$affected = $stm->rowCount();
if ($affected > 0) {
$sql = 'UPDATE `' . $this->prefix . 'feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
. 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
$values = array($ids);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error markRead 2: ' . $info[2]);
$this->bd->rollBack();
return false;
}
}
$this->bd->commit();
return $affected;
}
}
/**
* Mark all entries as read depending on parameters.
* If $onlyFavorites is true, it is used when the user mark as read in
* the favorite pseudo-category.
* If $priorityMin is greater than 0, it is used when the user mark as
* read in the main feed pseudo-category.
* Then the cache is updated.
*
* If $idMax equals 0, a deprecated debug message is logged
*
* @todo refactor this method along with markReadCat and markReadFeed
* since they are all doing the same thing. I think we need to build a
* tool to generate the query instead of having queries all over the
* place. It will be reused also for the filtering making every thing
* separated.
*
* @param integer $idMax fail safe article ID
* @param boolean $onlyFavorites
* @param integer $priorityMin
* @return integer affected rows
*/
public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
}
$sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=1 WHERE is_read=0 AND id <= ?';
if ($onlyFavorites) {
$sql .= ' AND is_favorite=1';
} elseif ($priorityMin >= 0) {
$sql .= ' AND id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
}
$values = array($idMax);
list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error markReadEntries: ' . $info[2]);
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
return false;
}
return $affected;
}
/**
* Mark all the articles in a category as read.
* There is a fail safe to prevent to mark as read articles that are
* loaded during the mark as read action. Then the cache is updated.
*
* If $idMax equals 0, a deprecated debug message is logged
*
* @param integer $id category ID
* @param integer $idMax fail safe article ID
* @return integer affected rows
*/
public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadCat(0) is deprecated!');
}
$sql = 'UPDATE `' . $this->prefix . 'entry` '
. 'SET is_read=1 '
. 'WHERE is_read=0 AND id <= ? AND '
. 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)';
$values = array($idMax, $id);
list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error markReadCat: ' . $info[2]);
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
return false;
}
return $affected;
}
public function optimizeTable() {
//TODO: Search for an equivalent in SQLite
}
public function size($all = false) {
return @filesize(join_path(DATA_PATH, 'users', $this->current_user, 'db.sqlite'));
}
}

52
app/Models/Factory.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
class FreshRSS_Factory {
public static function createFeedDao($username = null) {
$conf = Minz_Configuration::get('system');
switch ($conf->db['type']) {
case 'sqlite':
case 'pgsql':
return new FreshRSS_FeedDAOSQLite($username);
default:
return new FreshRSS_FeedDAO($username);
}
}
public static function createEntryDao($username = null) {
$conf = Minz_Configuration::get('system');
switch ($conf->db['type']) {
case 'sqlite':
return new FreshRSS_EntryDAOSQLite($username);
case 'pgsql':
return new FreshRSS_EntryDAOPGSQL($username);
default:
return new FreshRSS_EntryDAO($username);
}
}
public static function createStatsDAO($username = null) {
$conf = Minz_Configuration::get('system');
switch ($conf->db['type']) {
case 'sqlite':
return new FreshRSS_StatsDAOSQLite($username);
case 'pgsql':
return new FreshRSS_StatsDAOPGSQL($username);
default:
return new FreshRSS_StatsDAO($username);
}
}
public static function createDatabaseDAO($username = null) {
$conf = Minz_Configuration::get('system');
switch ($conf->db['type']) {
case 'sqlite':
return new FreshRSS_DatabaseDAOSQLite($username);
case 'pgsql':
return new FreshRSS_DatabaseDAOPGSQL($username);
default:
return new FreshRSS_DatabaseDAO($username);
}
}
}

View File

@@ -16,98 +16,141 @@ class FreshRSS_Feed extends Minz_Model {
private $httpAuth = '';
private $error = false;
private $keep_history = -2;
private $ttl = -2;
private $hash = null;
private $lockPath = '';
private $hubUrl = '';
private $selfUrl = '';
public function __construct ($url, $validate=true) {
public function __construct($url, $validate=true) {
if ($validate) {
$this->_url ($url);
$this->_url($url);
} else {
$this->url = $url;
}
}
public function id () {
public static function example() {
$f = new FreshRSS_Feed('http://example.net/', false);
$f->faviconPrepare();
return $f;
}
public function id() {
return $this->id;
}
public function hash() {
if ($this->hash === null) {
$this->hash = hash('crc32b', Minz_Configuration::salt() . $this->url);
$salt = FreshRSS_Context::$system_conf->salt;
$this->hash = hash('crc32b', $salt . $this->url);
}
return $this->hash;
}
public function url () {
public function url() {
return $this->url;
}
public function category () {
public function selfUrl() {
return $this->selfUrl;
}
public function hubUrl() {
return $this->hubUrl;
}
public function category() {
return $this->category;
}
public function entries () {
public function entries() {
return $this->entries === null ? array() : $this->entries;
}
public function name () {
public function name() {
return $this->name;
}
public function website () {
public function website() {
return $this->website;
}
public function description () {
public function description() {
return $this->description;
}
public function lastUpdate () {
public function lastUpdate() {
return $this->lastUpdate;
}
public function priority () {
public function priority() {
return $this->priority;
}
public function pathEntries () {
public function pathEntries() {
return $this->pathEntries;
}
public function httpAuth ($raw = true) {
public function httpAuth($raw = true) {
if ($raw) {
return $this->httpAuth;
} else {
$pos_colon = strpos ($this->httpAuth, ':');
$user = substr ($this->httpAuth, 0, $pos_colon);
$pass = substr ($this->httpAuth, $pos_colon + 1);
$pos_colon = strpos($this->httpAuth, ':');
$user = substr($this->httpAuth, 0, $pos_colon);
$pass = substr($this->httpAuth, $pos_colon + 1);
return array (
return array(
'username' => $user,
'password' => $pass
);
}
}
public function inError () {
public function inError() {
return $this->error;
}
public function keepHistory () {
public function keepHistory() {
return $this->keep_history;
}
public function nbEntries () {
public function ttl() {
return $this->ttl;
}
// public function ttlExpire() {
// $ttl = $this->ttl;
// if ($ttl == -2) { //Default
// $ttl = FreshRSS_Context::$user_conf->ttl_default;
// }
// if ($ttl == -1) { //Never
// $ttl = 64000000; //~2 years. Good enough for PubSubHubbub logic
// }
// return $this->lastUpdate + $ttl;
// }
public function nbEntries() {
if ($this->nbEntries < 0) {
$feedDAO = new FreshRSS_FeedDAO ();
$this->nbEntries = $feedDAO->countEntries ($this->id ());
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->nbEntries = $feedDAO->countEntries($this->id());
}
return $this->nbEntries;
}
public function nbNotRead () {
public function nbNotRead() {
if ($this->nbNotRead < 0) {
$feedDAO = new FreshRSS_FeedDAO ();
$this->nbNotRead = $feedDAO->countNotRead ($this->id ());
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->nbNotRead = $feedDAO->countNotRead($this->id());
}
return $this->nbNotRead;
}
public function faviconPrepare() {
$file = DATA_PATH . '/favicons/' . $this->hash() . '.txt';
if (!file_exists ($file)) {
$t = $this->website;
if (empty($t)) {
$t = $this->url;
global $favicons_dir;
require_once(LIB_PATH . '/favicons.php');
$url = $this->website;
if ($url == '') {
$url = $this->url;
}
$txt = $favicons_dir . $this->hash() . '.txt';
if (!file_exists($txt)) {
file_put_contents($txt, $url);
}
if (FreshRSS_Context::$isCli) {
$ico = $favicons_dir . $this->hash() . '.ico';
$ico_mtime = @filemtime($ico);
$txt_mtime = @filemtime($txt);
if ($txt_mtime != false &&
($ico_mtime == false || $ico_mtime < $txt_mtime || ($ico_mtime < time() - (14 * 86400)))) {
// no ico file or we should download a new one.
$url = file_get_contents($txt);
download_favicon($url, $ico) || touch($ico);
}
file_put_contents($file, $t);
}
}
public static function faviconDelete($hash) {
@@ -115,113 +158,134 @@ class FreshRSS_Feed extends Minz_Model {
@unlink($path . '.ico');
@unlink($path . '.txt');
}
public function favicon () {
return Minz_Url::display ('/f.php?' . $this->hash());
public function favicon() {
return Minz_Url::display('/f.php?' . $this->hash());
}
public function _id ($value) {
public function _id($value) {
$this->id = $value;
}
public function _url ($value, $validate=true) {
public function _url($value, $validate=true) {
$this->hash = null;
if ($validate) {
$value = checkUrl($value);
}
if (empty ($value)) {
throw new FreshRSS_BadUrl_Exception ($value);
if (empty($value)) {
throw new FreshRSS_BadUrl_Exception($value);
}
$this->url = $value;
}
public function _category ($value) {
public function _category($value) {
$value = intval($value);
$this->category = $value >= 0 ? $value : 0;
}
public function _name ($value) {
public function _name($value) {
$this->name = $value === null ? '' : $value;
}
public function _website ($value, $validate=true) {
public function _website($value, $validate=true) {
if ($validate) {
$value = checkUrl($value);
}
if (empty ($value)) {
if (empty($value)) {
$value = '';
}
$this->website = $value;
}
public function _description ($value) {
public function _description($value) {
$this->description = $value === null ? '' : $value;
}
public function _lastUpdate ($value) {
public function _lastUpdate($value) {
$this->lastUpdate = $value;
}
public function _priority ($value) {
public function _priority($value) {
$value = intval($value);
$this->priority = $value >= 0 ? $value : 10;
}
public function _pathEntries ($value) {
public function _pathEntries($value) {
$this->pathEntries = $value;
}
public function _httpAuth ($value) {
public function _httpAuth($value) {
$this->httpAuth = $value;
}
public function _error ($value) {
public function _error($value) {
$this->error = (bool)$value;
}
public function _keepHistory ($value) {
public function _keepHistory($value) {
$value = intval($value);
$value = min($value, 1000000);
$value = max($value, -2);
$this->keep_history = $value;
}
public function _nbNotRead ($value) {
public function _ttl($value) {
$value = intval($value);
$value = min($value, 100000000);
$value = max($value, -2);
$this->ttl = $value;
}
public function _nbNotRead($value) {
$this->nbNotRead = intval($value);
}
public function _nbEntries ($value) {
public function _nbEntries($value) {
$this->nbEntries = intval($value);
}
public function load ($loadDetails = false) {
public function load($loadDetails = false, $noCache = false) {
if ($this->url !== null) {
if (CACHE_PATH === false) {
throw new Minz_FileNotExistException (
throw new Minz_FileNotExistException(
'CACHE_PATH',
Minz_Exception::ERROR
);
} else {
$url = htmlspecialchars_decode ($this->url, ENT_QUOTES);
$url = htmlspecialchars_decode($this->url, ENT_QUOTES);
if ($this->httpAuth != '') {
$url = preg_replace ('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
}
$feed = customSimplePie();
$feed->set_feed_url ($url);
if (substr($url, -11) === '#force_feed') {
$feed->force_feed(true);
$url = substr($url, 0, -11);
}
$feed->set_feed_url($url);
if (!$loadDetails) { //Only activates auto-discovery when adding a new feed
$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
}
$mtime = $feed->init();
if ((!$mtime) || $feed->error()) {
throw new FreshRSS_Feed_Exception ($feed->error() . ' [' . $url . ']');
$errorMessage = $feed->error();
throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Unknown error for feed' : $errorMessage) . ' [' . $url . ']');
}
// si on a utilisé l'auto-discover, notre url va avoir changé
$subscribe_url = $feed->subscribe_url ();
if ($subscribe_url !== null && $subscribe_url !== $this->url) {
if ($this->httpAuth != '') {
// on enlève les id si authentification HTTP
$subscribe_url = preg_replace ('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url);
}
$this->_url ($subscribe_url);
}
$links = $feed->get_links('self');
$this->selfUrl = isset($links[0]) ? $links[0] : null;
$links = $feed->get_links('hub');
$this->hubUrl = isset($links[0]) ? $links[0] : null;
if ($loadDetails) {
$title = htmlspecialchars(html_only_entity_decode($feed->get_title()), ENT_COMPAT, 'UTF-8');
$this->_name ($title === null ? $this->url : $title);
// si on a utilisé l'auto-discover, notre url va avoir changé
$subscribe_url = $feed->subscribe_url(false);
$title = strtr(html_only_entity_decode($feed->get_title()), array('<' => '&lt;', '>' => '&gt;', '"' => '&quot;')); //HTML to HTML-PRE //ENT_COMPAT except &
$this->_name($title == '' ? $url : $title);
$this->_website(html_only_entity_decode($feed->get_link()));
$this->_description(html_only_entity_decode($feed->get_description()));
} else {
//The case of HTTP 301 Moved Permanently
$subscribe_url = $feed->subscribe_url(true);
}
if (($mtime === true) || ($mtime > $this->lastUpdate)) {
syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url);
$clean_url = SimplePie_Misc::url_remove_credentials($subscribe_url);
if ($subscribe_url !== null && $subscribe_url !== $url) {
$this->_url($clean_url);
}
if (($mtime === true) || ($mtime > $this->lastUpdate) || $noCache) {
//Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
$this->loadEntries($feed); // et on charge les articles du flux
} else {
syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url);
//Minz_Log::debug('FreshRSS use cache for ' . $clean_url);
$this->entries = array();
}
@@ -231,47 +295,54 @@ class FreshRSS_Feed extends Minz_Model {
}
}
private function loadEntries ($feed) {
$entries = array ();
public function loadEntries($feed) {
$entries = array();
foreach ($feed->get_items () as $item) {
$title = html_only_entity_decode (strip_tags ($item->get_title ()));
$author = $item->get_author ();
$link = $item->get_permalink ();
$date = @strtotime ($item->get_date ());
foreach ($feed->get_items() as $item) {
$title = html_only_entity_decode(strip_tags($item->get_title()));
$author = $item->get_author();
$link = $item->get_permalink();
$date = @strtotime($item->get_date());
// gestion des tags (catégorie == tag)
$tags_tmp = $item->get_categories ();
$tags = array ();
$tags_tmp = $item->get_categories();
$tags = array();
if ($tags_tmp !== null) {
foreach ($tags_tmp as $tag) {
$tags[] = html_only_entity_decode ($tag->get_label ());
$tags[] = html_only_entity_decode($tag->get_label());
}
}
$content = html_only_entity_decode ($item->get_content ());
$content = html_only_entity_decode($item->get_content());
$elinks = array();
foreach ($item->get_enclosures() as $enclosure) {
$elink = $enclosure->get_link();
if (array_key_exists($elink, $elinks)) continue;
$elinks[$elink] = '1';
$mime = strtolower($enclosure->get_type());
if (strpos($mime, 'image/') === 0) {
$content .= '<br /><img src="' . $elink . '" alt="" />';
if (empty($elinks[$elink])) {
$elinks[$elink] = '1';
$mime = strtolower($enclosure->get_type());
if (strpos($mime, 'image/') === 0) {
$content .= '<p class="enclosure"><img src="' . $elink . '" alt="" /></p>';
} elseif (strpos($mime, 'audio/') === 0) {
$content .= '<p class="enclosure"><audio preload="none" src="' . $elink . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
} elseif (strpos($mime, 'video/') === 0) {
$content .= '<p class="enclosure"><video preload="none" src="' . $elink . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
} else {
unset($elinks[$elink]);
}
}
}
$entry = new FreshRSS_Entry (
$this->id (),
$item->get_id (),
$entry = new FreshRSS_Entry(
$this->id(),
$item->get_id(),
$title === null ? '' : $title,
$author === null ? '' : html_only_entity_decode ($author->name),
$author === null ? '' : html_only_entity_decode($author->name),
$content === null ? '' : $content,
$link === null ? '' : $link,
$date ? $date : time ()
$date ? $date : time()
);
$entry->_tags ($tags);
$entry->_tags($tags);
// permet de récupérer le contenu des flux tronqués
$entry->loadCompleteContent($this->pathEntries());
@@ -282,21 +353,155 @@ class FreshRSS_Feed extends Minz_Model {
$this->entries = $entries;
}
function cacheModifiedTime() {
return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc');
}
function lock() {
$lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock';
if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) {
@unlink($lock);
$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
@unlink($this->lockPath);
}
if (($handle = @fopen($lock, 'x')) === false) {
if (($handle = @fopen($this->lockPath, 'x')) === false) {
return false;
}
//register_shutdown_function('unlink', $lock);
//register_shutdown_function('unlink', $this->lockPath);
@fclose($handle);
return true;
}
function unlock() {
$lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock';
@unlink($lock);
@unlink($this->lockPath);
}
//<PubSubHubbub>
function pubSubHubbubEnabled() {
$url = $this->selfUrl ? $this->selfUrl : $this->url;
$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
if ($hubFile = @file_get_contents($hubFilename)) {
$hubJson = json_decode($hubFile, true);
if ($hubJson && empty($hubJson['error']) &&
(empty($hubJson['lease_end']) || $hubJson['lease_end'] > time())) {
return true;
}
}
return false;
}
function pubSubHubbubError($error = true) {
$url = $this->selfUrl ? $this->selfUrl : $this->url;
$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
$hubFile = @file_get_contents($hubFilename);
$hubJson = $hubFile ? json_decode($hubFile, true) : array();
if (!isset($hubJson['error']) || $hubJson['error'] !== (bool)$error) {
$hubJson['error'] = (bool)$error;
file_put_contents($hubFilename, json_encode($hubJson));
file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t"
. 'Set error to ' . ($error ? 1 : 0) . ' for ' . $url . "\n", FILE_APPEND);
}
return false;
}
function pubSubHubbubPrepare() {
$key = '';
if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) {
$path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl);
$hubFilename = $path . '/!hub.json';
if ($hubFile = @file_get_contents($hubFilename)) {
$hubJson = json_decode($hubFile, true);
if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
$text = 'Invalid JSON for PubSubHubbub: ' . $this->url;
Minz_Log::warning($text);
file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
return false;
}
if ((!empty($hubJson['lease_end'])) && ($hubJson['lease_end'] < (time() + (3600 * 23)))) { //TODO: Make a better policy
$text = 'PubSubHubbub lease ends at '
. date('c', empty($hubJson['lease_end']) ? time() : $hubJson['lease_end'])
. ' and needs renewal: ' . $this->url;
Minz_Log::warning($text);
file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
$key = $hubJson['key']; //To renew our lease
} elseif (((!empty($hubJson['error'])) || empty($hubJson['lease_end'])) &&
(empty($hubJson['lease_start']) || $hubJson['lease_start'] < time() - (3600 * 23))) { //Do not renew too often
$key = $hubJson['key']; //To renew our lease
}
} else {
@mkdir($path, 0777, true);
$key = sha1($path . FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true));
$hubJson = array(
'hub' => $this->hubUrl,
'key' => $key,
);
file_put_contents($hubFilename, json_encode($hubJson));
@mkdir(PSHB_PATH . '/keys/');
file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', base64url_encode($this->selfUrl));
$text = 'PubSubHubbub prepared for ' . $this->url;
Minz_Log::debug($text);
file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
}
$currentUser = Minz_Session::param('currentUser');
if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) {
touch($path . '/' . $currentUser . '.txt');
}
}
return $key;
}
//Parameter true to subscribe, false to unsubscribe.
function pubSubHubbubSubscribe($state) {
if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) {
$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json';
$hubFile = @file_get_contents($hubFilename);
if ($hubFile === false) {
Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url);
return false;
}
$hubJson = json_decode($hubFile, true);
if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url);
return false;
}
$callbackUrl = checkUrl(Minz_Request::getBaseUrl() . '/api/pshb.php?k=' . $hubJson['key']);
if ($callbackUrl == '') {
Minz_Log::warning('Invalid callback for PubSubHubbub: ' . $this->url);
return false;
}
if (!$state) { //unsubscribe
$hubJson['lease_end'] = time() - 60;
file_put_contents($hubFilename, json_encode($hubJson));
}
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $this->hubUrl,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
CURLOPT_POSTFIELDS => 'hub.verify=sync'
. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
. '&hub.topic=' . urlencode($this->selfUrl)
. '&hub.callback=' . urlencode($callbackUrl)
)
);
$response = curl_exec($ch);
$info = curl_getinfo($ch);
file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" .
'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl .
' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND);
if (substr($info['http_code'], 0, 1) == '2') {
return true;
} else {
$hubJson['lease_start'] = time(); //Prevent trying again too soon
$hubJson['error'] = true;
file_put_contents($hubFilename, json_encode($hubJson));
return false;
}
}
return false;
}
//</PubSubHubbub>
}

View File

@@ -1,244 +1,300 @@
<?php
class FreshRSS_FeedDAO extends Minz_ModelPdo {
public function addFeed ($valuesTmp) {
$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2)';
$stm = $this->bd->prepare ($sql);
class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function addFeed($valuesTmp) {
$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, `lastUpdate`, priority, `httpAuth`, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
$stm = $this->bd->prepare($sql);
$values = array (
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
$valuesTmp['website'] = safe_ascii($valuesTmp['website']);
$values = array(
substr($valuesTmp['url'], 0, 511),
$valuesTmp['category'],
substr($valuesTmp['name'], 0, 255),
substr($valuesTmp['website'], 0, 255),
substr($valuesTmp['description'], 0, 1023),
$valuesTmp['lastUpdate'],
base64_encode ($valuesTmp['httpAuth']),
base64_encode($valuesTmp['httpAuth']),
);
if ($stm && $stm->execute ($values)) {
return $this->bd->lastInsertId();
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error addFeed: ' . $info[2]);
return false;
}
}
public function updateFeed ($id, $valuesTmp) {
public function addFeedObject($feed) {
// TODO: not sure if we should write this method in DAO since DAO
// should not be aware about feed class
// Add feed only if we don't find it in DB
$feed_search = $this->searchByUrl($feed->url());
if (!$feed_search) {
$values = array(
'id' => $feed->id(),
'url' => $feed->url(),
'category' => $feed->category(),
'name' => $feed->name(),
'website' => $feed->website(),
'description' => $feed->description(),
'lastUpdate' => 0,
'httpAuth' => $feed->httpAuth()
);
$id = $this->addFeed($values);
if ($id) {
$feed->_id($id);
$feed->faviconPrepare();
}
return $id;
}
return $feed_search->id();
}
public function updateFeed($id, $valuesTmp) {
if (isset($valuesTmp['url'])) {
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
}
if (isset($valuesTmp['website'])) {
$valuesTmp['website'] = safe_ascii($valuesTmp['website']);
}
$set = '';
foreach ($valuesTmp as $key => $v) {
$set .= $key . '=?, ';
if ($key == 'httpAuth') {
$valuesTmp[$key] = base64_encode ($v);
$valuesTmp[$key] = base64_encode($v);
}
}
$set = substr ($set, 0, -2);
$set = substr($set, 0, -2);
$sql = 'UPDATE `' . $this->prefix . 'feed` SET ' . $set . ' WHERE id=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
foreach ($valuesTmp as $v) {
$values[] = $v;
}
$values[] = $id;
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error updateFeed: ' . $info[2]);
return false;
}
}
public function updateLastUpdate ($id, $inError = 0, $updateCache = true) {
public function updateLastUpdate($id, $inError = false, $updateCache = true, $mtime = 0) {
if ($updateCache) {
$sql = 'UPDATE `' . $this->prefix . 'feed` f ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
. 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),'
. 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),'
. 'lastUpdate=?, error=? '
. 'WHERE f.id=?';
$sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
. 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
. '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
. '`lastUpdate`=?, error=? '
. 'WHERE id=?';
} else {
$sql = 'UPDATE `' . $this->prefix . 'feed` f '
. 'SET lastUpdate=?, error=? '
. 'WHERE f.id=?';
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET `lastUpdate`=?, error=? '
. 'WHERE id=?';
}
$values = array (
time(),
$inError,
if ($mtime <= 0) {
$mtime = time();
}
$values = array(
$mtime,
$inError ? 1 : 0,
$id,
);
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error updateLastUpdate: ' . $info[2]);
return false;
}
}
public function changeCategory ($idOldCat, $idNewCat) {
$catDAO = new FreshRSS_CategoryDAO ();
$newCat = $catDAO->searchById ($idNewCat);
public function changeCategory($idOldCat, $idNewCat) {
$catDAO = new FreshRSS_CategoryDAO();
$newCat = $catDAO->searchById($idNewCat);
if (!$newCat) {
$newCat = $catDAO->getDefault ();
$newCat = $catDAO->getDefault();
}
$sql = 'UPDATE `' . $this->prefix . 'feed` SET category=? WHERE category=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array (
$newCat->id (),
$values = array(
$newCat->id(),
$idOldCat
);
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error changeCategory: ' . $info[2]);
return false;
}
}
public function deleteFeed ($id) {
/*//For MYISAM (MySQL 5.5-) without FOREIGN KEY
$sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
if (!($stm && $stm->execute ($values))) {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
return false;
}*/
public function deleteFeed($id) {
$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE id=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($id);
$values = array($id);
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error deleteFeed: ' . $info[2]);
return false;
}
}
public function deleteFeedByCategory ($id) {
/*//For MYISAM (MySQL 5.5-) without FOREIGN KEY
$sql = 'DELETE FROM `' . $this->prefix . 'entry` e '
. 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
. 'WHERE f.category=?';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
if (!($stm && $stm->execute ($values))) {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
return false;
}*/
public function deleteFeedByCategory($id) {
$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE category=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($id);
$values = array($id);
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error deleteFeedByCategory: ' . $info[2]);
return false;
}
}
public function searchById ($id) {
public function searchById($id) {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE id=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($id);
$values = array($id);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$feed = self::daoToFeed ($res);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$feed = self::daoToFeed($res);
if (isset ($feed[$id])) {
if (isset($feed[$id])) {
return $feed[$id];
} else {
return false;
return null;
}
}
public function searchByUrl ($url) {
public function searchByUrl($url) {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE url=?';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($url);
$values = array($url);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$feed = current (self::daoToFeed ($res));
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$feed = current(self::daoToFeed($res));
if (isset ($feed)) {
if (isset($feed) && $feed !== false) {
return $feed;
} else {
return false;
return null;
}
}
public function listFeeds () {
public function listFeedsIds() {
$sql = 'SELECT id FROM `' . $this->prefix . 'feed`';
$stm = $this->bd->prepare($sql);
$stm->execute();
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
}
public function listFeeds() {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
$stm = $this->bd->prepare ($sql);
$stm->execute ();
$stm = $this->bd->prepare($sql);
$stm->execute();
return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
}
public function listFeedsOrderUpdate ($cacheDuration = 1500) {
$sql = 'SELECT id, name, url, lastUpdate, pathEntries, httpAuth, keep_history '
public function arrayFeedCategoryNames() { //For API
$sql = 'SELECT f.id, f.name, c.name as c_name FROM `' . $this->prefix . 'feed` f '
. 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category';
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$feedCategoryNames = array();
foreach ($res as $line) {
$feedCategoryNames[$line['id']] = array(
'name' => $line['name'],
'c_name' => $line['c_name'],
);
}
return $feedCategoryNames;
}
/**
* Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
*/
public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl '
. 'FROM `' . $this->prefix . 'feed` '
. 'WHERE lastUpdate < ' . (time() - intval($cacheDuration))
. ' ORDER BY lastUpdate';
$stm = $this->bd->prepare ($sql);
$stm->execute ();
. ($defaultCacheDuration < 0 ? '' : 'WHERE ttl <> -1 AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
. 'ORDER BY `lastUpdate`';
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute())) {
$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2'; //v0.7.3
$stm = $this->bd->prepare($sql2);
$stm->execute();
$stm = $this->bd->prepare($sql);
$stm->execute();
}
return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
}
public function listByCategory ($cat) {
public function listByCategory($cat) {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE category=? ORDER BY name';
$stm = $this->bd->prepare ($sql);
$stm = $this->bd->prepare($sql);
$values = array ($cat);
$values = array($cat);
$stm->execute ($values);
$stm->execute($values);
return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC));
return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
}
public function countEntries ($id) {
public function countEntries($id) {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $res[0]['count'];
}
public function countNotRead ($id) {
public function countNotRead($id) {
$sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND is_read=0';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $res[0]['count'];
}
public function updateCachedValues () { //For one single feed, call updateLastUpdate($id)
public function updateCachedValues() { //For one single feed, call updateLastUpdate($id)
$sql = 'UPDATE `' . $this->prefix . 'feed` f '
. 'INNER JOIN ('
. 'SELECT e.id_feed, '
@@ -247,81 +303,82 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
. 'FROM `' . $this->prefix . 'entry` e '
. 'GROUP BY e.id_feed'
. ') x ON x.id_feed=f.id '
. 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
$stm = $this->bd->prepare ($sql);
. 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads';
$stm = $this->bd->prepare($sql);
$values = array ($feed_id);
if ($stm && $stm->execute ($values)) {
if ($stm && $stm->execute()) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error updateCachedValues: ' . $info[2]);
return false;
}
}
public function truncate ($id) {
$sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e WHERE e.id_feed=?';
public function truncate($id) {
$sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?';
$stm = $this->bd->prepare($sql);
$values = array($id);
$this->bd->beginTransaction ();
if (!($stm && $stm->execute ($values))) {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$this->bd->rollBack ();
return false;
}
$this->bd->beginTransaction();
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error truncate: ' . $info[2]);
$this->bd->rollBack();
return false;
}
$affected = $stm->rowCount();
$sql = 'UPDATE `' . $this->prefix . 'feed` f '
. 'SET f.cache_nbEntries=0, f.cache_nbUnreads=0 WHERE f.id=?';
$values = array ($id);
$stm = $this->bd->prepare ($sql);
if (!($stm && $stm->execute ($values))) {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$this->bd->rollBack ();
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=?';
$values = array($id);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error truncate: ' . $info[2]);
$this->bd->rollBack();
return false;
}
$this->bd->commit ();
$this->bd->commit();
return $affected;
}
public function cleanOldEntries ($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) just after
$sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e '
. 'WHERE e.id_feed = :id_feed AND e.id <= :id_max AND e.is_favorite = 0 AND e.id NOT IN '
. '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select because of: MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
$stm = $this->bd->prepare ($sql);
public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) or updateCachedValues() just after
$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
. 'WHERE id_feed=:id_feed AND id<=:id_max '
. 'AND is_favorite=0 ' //Do not remove favourites
. 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance
. 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
$stm = $this->bd->prepare($sql);
$id_max = intval($date_min) . '000000';
if ($stm) {
$id_max = intval($date_min) . '000000';
$stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
$stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
}
$stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
$stm->bindParam(':id_max', $id_max, PDO::PARAM_INT);
$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
if ($stm && $stm->execute ()) {
if ($stm && $stm->execute()) {
return $stm->rowCount();
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error cleanOldEntries: ' . $info[2]);
return false;
}
}
public static function daoToFeed ($listDAO, $catID = null) {
$list = array ();
public static function daoToFeed($listDAO, $catID = null) {
$list = array();
if (!is_array ($listDAO)) {
$listDAO = array ($listDAO);
if (!is_array($listDAO)) {
$listDAO = array($listDAO);
}
foreach ($listDAO as $key => $dao) {
if (!isset ($dao['name'])) {
if (!isset($dao['name'])) {
continue;
}
if (isset ($dao['id'])) {
if (isset($dao['id'])) {
$key = $dao['id'];
}
if ($catID === null) {
@@ -338,13 +395,14 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
$myFeed->_lastUpdate(isset($dao['lastUpdate']) ? $dao['lastUpdate'] : 0);
$myFeed->_priority(isset($dao['priority']) ? $dao['priority'] : 10);
$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode ($dao['httpAuth']) : '');
$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
$myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : -2);
$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : -2);
$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);
$myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0);
if (isset ($dao['id'])) {
$myFeed->_id ($dao['id']);
if (isset($dao['id'])) {
$myFeed->_id($dao['id']);
}
$list[$key] = $myFeed;
}

View File

@@ -0,0 +1,19 @@
<?php
class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
public function updateCachedValues() { //For one single feed, call updateLastUpdate($id)
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
. '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute()) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error updateCachedValues: ' . $info[2]);
return false;
}
}
}

View File

@@ -5,22 +5,22 @@ class FreshRSS_Log extends Minz_Model {
private $level;
private $information;
public function date () {
public function date() {
return $this->date;
}
public function level () {
public function level() {
return $this->level;
}
public function info () {
public function info() {
return $this->information;
}
public function _date ($date) {
public function _date($date) {
$this->date = $date;
}
public function _level ($level) {
public function _level($level) {
$this->level = $level;
}
public function _info ($information) {
public function _info($information) {
$this->information = $information;
}
}

View File

@@ -2,15 +2,15 @@
class FreshRSS_LogDAO {
public static function lines() {
$logs = array ();
$handle = @fopen(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', 'r');
$logs = array();
$handle = @fopen(join_path(DATA_PATH, 'users', Minz_Session::param('currentUser', '_'), 'log.txt'), 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
if (preg_match ('/^\[([^\[]+)\] \[([^\[]+)\] --- (.*)$/', $line, $matches)) {
if (preg_match('/^\[([^\[]+)\] \[([^\[]+)\] --- (.*)$/', $line, $matches)) {
$myLog = new FreshRSS_Log ();
$myLog->_date ($matches[1]);
$myLog->_level ($matches[2]);
$myLog->_info ($matches[3]);
$myLog->_date($matches[1]);
$myLog->_level($matches[2]);
$myLog->_info($matches[3]);
$logs[] = $myLog;
}
}
@@ -20,6 +20,11 @@ class FreshRSS_LogDAO {
}
public static function truncate() {
file_put_contents(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', '');
file_put_contents(join_path(DATA_PATH, 'users', Minz_Session::param('currentUser', '_'), 'log.txt'), '');
if (FreshRSS_Auth::hasAccess('admin')) {
file_put_contents(join_path(DATA_PATH, 'users', '_', 'log.txt'), '');
file_put_contents(join_path(DATA_PATH, 'users', '_', 'log_api.txt'), '');
file_put_contents(join_path(DATA_PATH, 'users', '_', 'log_pshb.txt'), '');
}
}
}

229
app/Models/Search.php Normal file
View File

@@ -0,0 +1,229 @@
<?php
require_once(LIB_PATH . '/lib_date.php');
/**
* Contains a search from the search form.
*
* It allows to extract meaningful bits of the search and store them in a
* convenient object
*/
class FreshRSS_Search {
// This contains the user input string
private $raw_input = '';
// The following properties are extracted from the raw input
private $intitle;
private $min_date;
private $max_date;
private $min_pubdate;
private $max_pubdate;
private $inurl;
private $author;
private $tags;
private $search;
public function __construct($input) {
if (strcmp($input, '') == 0) {
return;
}
$this->raw_input = $input;
$input = $this->parseIntitleSearch($input);
$input = $this->parseAuthorSearch($input);
$input = $this->parseInurlSearch($input);
$input = $this->parsePubdateSearch($input);
$input = $this->parseDateSearch($input);
$input = $this->parseTagsSeach($input);
$this->parseSearch($input);
}
public function __toString() {
return $this->getRawInput();
}
public function getRawInput() {
return $this->raw_input;
}
public function getIntitle() {
return $this->intitle;
}
public function getMinDate() {
return $this->min_date;
}
public function getMaxDate() {
return $this->max_date;
}
public function getMinPubdate() {
return $this->min_pubdate;
}
public function getMaxPubdate() {
return $this->max_pubdate;
}
public function getInurl() {
return $this->inurl;
}
public function getAuthor() {
return $this->author;
}
public function getTags() {
return $this->tags;
}
public function getSearch() {
return $this->search;
}
/**
* Parse the search string to find intitle keyword and the search related
* to it.
* The search is the first word following the keyword.
*
* @param string $input
* @return string
*/
private function parseIntitleSearch($input) {
if (preg_match('/intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->intitle = $matches['search'];
return str_replace($matches[0], '', $input);
}
if (preg_match('/intitle:(?P<search>\w*)/', $input, $matches)) {
$this->intitle = $matches['search'];
return str_replace($matches[0], '', $input);
}
return $input;
}
/**
* Parse the search string to find author keyword and the search related
* to it.
* The search is the first word following the keyword except when using
* a delimiter. Supported delimiters are single quote (') and double
* quotes (").
*
* @param string $input
* @return string
*/
private function parseAuthorSearch($input) {
if (preg_match('/author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->author = $matches['search'];
return str_replace($matches[0], '', $input);
}
if (preg_match('/author:(?P<search>\w*)/', $input, $matches)) {
$this->author = $matches['search'];
return str_replace($matches[0], '', $input);
}
return $input;
}
/**
* Parse the search string to find inurl keyword and the search related
* to it.
* The search is the first word following the keyword except.
*
* @param string $input
* @return string
*/
private function parseInurlSearch($input) {
if (preg_match('/inurl:(?P<search>[^\s]*)/', $input, $matches)) {
$this->inurl = $matches['search'];
return str_replace($matches[0], '', $input);
}
return $input;
}
/**
* Parse the search string to find date keyword and the search related
* to it.
* The search is the first word following the keyword.
*
* @param string $input
* @return string
*/
private function parseDateSearch($input) {
if (preg_match('/date:(?P<search>[^\s]*)/', $input, $matches)) {
list($this->min_date, $this->max_date) = parseDateInterval($matches['search']);
return str_replace($matches[0], '', $input);
}
return $input;
}
/**
* Parse the search string to find pubdate keyword and the search related
* to it.
* The search is the first word following the keyword.
*
* @param string $input
* @return string
*/
private function parsePubdateSearch($input) {
if (preg_match('/pubdate:(?P<search>[^\s]*)/', $input, $matches)) {
list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($matches['search']);
return str_replace($matches[0], '', $input);
}
return $input;
}
/**
* Parse the search string to find tags keyword (# followed by a word)
* and the search related to it.
* The search is the first word following the #.
*
* @param string $input
* @return string
*/
private function parseTagsSeach($input) {
if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
$this->tags = $matches['search'];
return str_replace($matches[0], '', $input);
}
return $input;
}
/**
* Parse the search string to find search values.
* Every word is a distinct search value, except when using a delimiter.
* Supported delimiters are single quote (') and double quotes (").
*
* @param string $input
* @return string
*/
private function parseSearch($input) {
$input = $this->cleanSearch($input);
if (strcmp($input, '') == 0) {
return;
}
if (preg_match_all('/(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->search = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
$input = $this->cleanSearch($input);
if (strcmp($input, '') == 0) {
return;
}
if (is_array($this->search)) {
$this->search = array_merge($this->search, explode(' ', $input));
} else {
$this->search = explode(' ', $input);
}
}
/**
* Remove all unnecessary spaces in the search
*
* @param string $input
* @return string
*/
private function cleanSearch($input) {
$input = preg_replace('/\s+/', ' ', $input);
return trim($input);
}
}

View File

@@ -0,0 +1,6 @@
<?php
interface FreshRSS_Searchable {
public function searchById($id);
}

238
app/Models/Share.php Normal file
View File

@@ -0,0 +1,238 @@
<?php
/**
* Manage the sharing options in FreshRSS.
*/
class FreshRSS_Share {
/**
* The list of available sharing options.
*/
private static $list_sharing = array();
/**
* Register a new sharing option.
* @param $share_options is an array defining the share option.
*/
public static function register($share_options) {
$type = $share_options['type'];
if (isset(self::$list_sharing[$type])) {
return;
}
$help_url = isset($share_options['help']) ? $share_options['help'] : '';
self::$list_sharing[$type] = new FreshRSS_Share(
$type, $share_options['url'], $share_options['transform'],
$share_options['form'], $help_url
);
}
/**
* Register sharing options in a file.
* @param $filename the name of the file to load.
*/
public static function load($filename) {
$shares_from_file = @include($filename);
if (!is_array($shares_from_file)) {
$shares_from_file = array();
}
foreach ($shares_from_file as $share_type => $share_options) {
$share_options['type'] = $share_type;
self::register($share_options);
}
}
/**
* Return the list of sharing options.
* @return an array of FreshRSS_Share objects.
*/
public static function enum() {
return self::$list_sharing;
}
/**
* Return FreshRSS_Share object related to the given type.
* @param $type the share type, null if $type is not registered.
*/
public static function get($type) {
if (!isset(self::$list_sharing[$type])) {
return null;
}
return self::$list_sharing[$type];
}
/**
*
*/
private $type = '';
private $name = '';
private $url_transform = '';
private $transform = array();
private $form_type = 'simple';
private $help_url = '';
private $custom_name = null;
private $base_url = null;
private $title = null;
private $link = null;
/**
* Create a FreshRSS_Share object.
* @param $type is a unique string defining the kind of share option.
* @param $url_transform defines the url format to use in order to share.
* @param $transform is an array of transformations to apply on link and title.
* @param $form_type defines which form we have to use to complete. "simple"
* is typically for a centralized service while "advanced" is for
* decentralized ones.
* @param $help_url is an optional url to give help on this option.
*/
private function __construct($type, $url_transform, $transform = array(),
$form_type, $help_url = '') {
$this->type = $type;
$this->name = _t('gen.share.' . $type);
$this->url_transform = $url_transform;
$this->help_url = $help_url;
if (!is_array($transform)) {
$transform = array();
}
$this->transform = $transform;
if (!in_array($form_type, array('simple', 'advanced'))) {
$form_type = 'simple';
}
$this->form_type = $form_type;
}
/**
* Update a FreshRSS_Share object with information from an array.
* @param $options is a list of informations to update where keys should be
* in this list: name, url, title, link.
*/
public function update($options) {
$available_options = array(
'name' => 'custom_name',
'url' => 'base_url',
'title' => 'title',
'link' => 'link',
);
foreach ($options as $key => $value) {
if (isset($available_options[$key])) {
$this->{$available_options[$key]} = $value;
}
}
}
/**
* Return the current type of the share option.
*/
public function type() {
return $this->type;
}
/**
* Return the current form type of the share option.
*/
public function formType() {
return $this->form_type;
}
/**
* Return the current help url of the share option.
*/
public function help() {
return $this->help_url;
}
/**
* Return the current name of the share option.
*/
public function name($real = false) {
if ($real || is_null($this->custom_name) || empty($this->custom_name)) {
return $this->name;
} else {
return $this->custom_name;
}
}
/**
* Return the current base url of the share option.
*/
public function baseUrl() {
return $this->base_url;
}
/**
* Return the current url by merging url_transform and base_url.
*/
public function url() {
$matches = array(
'~URL~',
'~TITLE~',
'~LINK~',
);
$replaces = array(
$this->base_url,
$this->title(),
$this->link(),
);
return str_replace($matches, $replaces, $this->url_transform);
}
/**
* Return the title.
* @param $raw true if we should get the title without transformations.
*/
public function title($raw = false) {
if ($raw) {
return $this->title;
}
return $this->transform($this->title, $this->getTransform('title'));
}
/**
* Return the link.
* @param $raw true if we should get the link without transformations.
*/
public function link($raw = false) {
if ($raw) {
return $this->link;
}
return $this->transform($this->link, $this->getTransform('link'));
}
/**
* Transform a data with the given functions.
* @param $data the data to transform.
* @param $tranform an array containing a list of functions to apply.
* @return the transformed data.
*/
private static function transform($data, $transform) {
if (!is_array($transform) || empty($transform)) {
return $data;
}
foreach ($transform as $action) {
$data = call_user_func($action, $data);
}
return $data;
}
/**
* Get the list of transformations for the given attribute.
* @param $attr the attribute of which we want the transformations.
* @return an array containing a list of transformations to apply.
*/
private function getTransform($attr) {
if (array_key_exists($attr, $this->transform)) {
return $this->transform[$attr];
}
return $this->transform;
}
}

View File

@@ -2,98 +2,79 @@
class FreshRSS_StatsDAO extends Minz_ModelPdo {
const ENTRY_COUNT_PERIOD = 30;
protected function sqlFloor($s) {
return "FLOOR($s)";
}
/**
* Calculates entry repartition for all feeds and for main stream.
*
* @return array
*/
public function calculateEntryRepartition() {
return array(
'main_stream' => $this->calculateEntryRepartitionPerFeed(null, true),
'all_feeds' => $this->calculateEntryRepartitionPerFeed(null, false),
);
}
/**
* Calculates entry repartition for the selection.
* The repartition includes:
* - total entries
* - read entries
* - unread entries
* - favorite entries
*
* @return type
*
* @param null|integer $feed feed id
* @param boolean $only_main
* @return array
*/
public function calculateEntryRepartition() {
$repartition = array();
// Generates the repartition for the main stream of entry
public function calculateEntryRepartitionPerFeed($feed = null, $only_main = false) {
$filter = '';
if ($only_main) {
$filter .= 'AND f.priority = 10';
}
if (!is_null($feed)) {
$filter .= "AND e.id_feed = {$feed}";
}
$sql = <<<SQL
SELECT COUNT(1) AS `total`,
COUNT(1) - SUM(e.is_read) AS `unread`,
SUM(e.is_read) AS `read`,
SUM(e.is_favorite) AS `favorite`
FROM {$this->prefix}entry AS e
, {$this->prefix}feed AS f
SELECT COUNT(1) AS total,
COUNT(1) - SUM(e.is_read) AS count_unreads,
SUM(e.is_read) AS count_reads,
SUM(e.is_favorite) AS count_favorites
FROM `{$this->prefix}entry` AS e
, `{$this->prefix}feed` AS f
WHERE e.id_feed = f.id
AND f.priority = 10
{$filter}
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$repartition['main_stream'] = $res[0];
// Generates the repartition for all entries
$sql = <<<SQL
SELECT COUNT(1) AS `total`,
COUNT(1) - SUM(e.is_read) AS `unread`,
SUM(e.is_read) AS `read`,
SUM(e.is_favorite) AS `favorite`
FROM {$this->prefix}entry AS e
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$repartition['all_feeds'] = $res[0];
return $repartition;
return $res[0];
}
/**
* Calculates entry count per day on a 30 days period.
* Returns the result as a JSON string.
*
* @return string
* Returns the result as a JSON object.
*
* @return JSON object
*/
public function calculateEntryCount() {
$count = array();
$count = $this->initEntryCountArray();
$midnight = mktime(0, 0, 0);
$oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
// Generates a list of 30 last day to be sure we always have 30 days.
// If we do not do that kind of thing, we'll end up with holes in the
// days if the user do not have a lot of feeds.
// Get stats per day for the last 30 days
$sqlDay = $this->sqlFloor("(date - $midnight) / 86400");
$sql = <<<SQL
SELECT - (tens.val + units.val + 1) AS day
FROM (
SELECT 0 AS val
UNION ALL SELECT 1
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
UNION ALL SELECT 5
UNION ALL SELECT 6
UNION ALL SELECT 7
UNION ALL SELECT 8
UNION ALL SELECT 9
) AS units
CROSS JOIN (
SELECT 0 AS val
UNION ALL SELECT 10
UNION ALL SELECT 20
) AS tens
ORDER BY day ASC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
foreach ($res as $value) {
$count[$value['day']] = 0;
}
// Get stats per day for the last 30 days and applies the result on
// the array created with the last query.
$sql = <<<SQL
SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day,
COUNT(1) AS count
FROM {$this->prefix}entry AS e
WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -30 DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d')
SELECT {$sqlDay} AS day,
COUNT(*) as count
FROM `{$this->prefix}entry`
WHERE date >= {$oldest} AND date < {$midnight}
GROUP BY day
ORDER BY day ASC
SQL;
@@ -105,21 +86,173 @@ SQL;
$count[$value['day']] = (int) $value['count'];
}
return $this->convertToSerie($count);
return $count;
}
/**
* Initialize an array for the entry count.
*
* @return array
*/
protected function initEntryCountArray() {
return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1);
}
/**
* Calculates the number of article per hour of the day per feed
*
* @param integer $feed id
* @return string
*/
public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
return $this->calculateEntryRepartitionPerFeedPerPeriod('%H', $feed);
}
/**
* Calculates the number of article per day of week per feed
*
* @param integer $feed id
* @return string
*/
public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
return $this->calculateEntryRepartitionPerFeedPerPeriod('%w', $feed);
}
/**
* Calculates the number of article per month per feed
*
* @param integer $feed
* @return string
*/
public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
return $this->calculateEntryRepartitionPerFeedPerPeriod('%m', $feed);
}
/**
* Calculates the number of article per period per feed
*
* @param string $period format string to use for grouping
* @param integer $feed id
* @return string
*/
protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
$restrict = '';
if ($feed) {
$restrict = "WHERE e.id_feed = {$feed}";
}
$sql = <<<SQL
SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
, COUNT(1) AS count
FROM `{$this->prefix}entry` AS e
{$restrict}
GROUP BY period
ORDER BY period ASC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_NAMED);
$repartition = array();
foreach ($res as $value) {
$repartition[(int) $value['period']] = (int) $value['count'];
}
return $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) {
$restrict = '';
if ($feed) {
$restrict = "WHERE e.id_feed = {$feed}";
}
$sql = <<<SQL
SELECT COUNT(1) AS count
, MIN(date) AS date_min
, MAX(date) AS date_max
FROM `{$this->prefix}entry` AS e
{$restrict}
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetch(PDO::FETCH_NAMED);
$date_min = new \DateTime();
$date_min->setTimestamp($res['date_min']);
$date_max = new \DateTime();
$date_max->setTimestamp($res['date_max']);
$interval = $date_max->diff($date_min, true);
$interval_in_days = $interval->format('%a');
if ($interval_in_days <= 0) {
// Surely only one article.
// We will return count / (period/period) == count.
$interval_in_days = $period;
}
return $res['count'] / ($interval_in_days / $period);
}
/**
* Initialize an array for statistics depending on a range
*
* @param integer $min
* @param integer $max
* @return array
*/
protected function initStatsArray($min, $max) {
return array_map(function () {
return 0;
}, array_flip(range($min, $max)));
}
/**
* Calculates feed count per category.
* Returns the result as a JSON string.
*
* @return string
* Returns the result as a JSON object.
*
* @return JSON object
*/
public function calculateFeedByCategory() {
$sql = <<<SQL
SELECT c.name AS label
, COUNT(f.id) AS data
FROM {$this->prefix}category AS c,
{$this->prefix}feed AS f
FROM `{$this->prefix}category` AS c,
`{$this->prefix}feed` AS f
WHERE c.id = f.category
GROUP BY label
ORDER BY data DESC
@@ -128,22 +261,22 @@ SQL;
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $this->convertToPieSerie($res);
return $res;
}
/**
* Calculates entry count per category.
* Returns the result as a JSON string.
*
* @return string
*
* @return JSON object
*/
public function calculateEntryByCategory() {
$sql = <<<SQL
SELECT c.name AS label
, COUNT(e.id) AS data
FROM {$this->prefix}category AS c,
{$this->prefix}feed AS f,
{$this->prefix}entry AS e
FROM `{$this->prefix}category` AS c,
`{$this->prefix}feed` AS f,
`{$this->prefix}entry` AS e
WHERE c.id = f.category
AND f.id = e.id_feed
GROUP BY label
@@ -153,12 +286,12 @@ SQL;
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $this->convertToPieSerie($res);
return $res;
}
/**
* Calculates the 10 top feeds based on their number of entries
*
*
* @return array
*/
public function calculateTopFeed() {
@@ -167,12 +300,12 @@ SELECT f.id AS id
, MAX(f.name) AS name
, MAX(c.name) AS category
, COUNT(e.id) AS count
FROM {$this->prefix}category AS c,
{$this->prefix}feed AS f,
{$this->prefix}entry AS e
FROM `{$this->prefix}category` AS c,
`{$this->prefix}feed` AS f,
`{$this->prefix}entry` AS e
WHERE c.id = f.category
AND f.id = e.id_feed
GROUP BY id
GROUP BY f.id
ORDER BY count DESC
LIMIT 10
SQL;
@@ -181,25 +314,79 @@ SQL;
return $stm->fetchAll(PDO::FETCH_ASSOC);
}
private function convertToSerie($data) {
$serie = array();
foreach ($data as $key => $value) {
$serie[] = array($key, $value);
}
return json_encode($serie);
/**
* Calculates the last publication date for each feed
*
* @return array
*/
public function calculateFeedLastDate() {
$sql = <<<SQL
SELECT MAX(f.id) as id
, MAX(f.name) AS name
, MAX(date) AS last_date
, COUNT(*) AS nb_articles
FROM `{$this->prefix}feed` AS f,
`{$this->prefix}entry` AS e
WHERE f.id = e.id_feed
GROUP BY f.id
ORDER BY name
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
return $stm->fetchAll(PDO::FETCH_ASSOC);
}
private function convertToPieSerie($data) {
$serie = array();
/**
* Gets days ready for graphs
*
* @return string
*/
public function getDays() {
return $this->convertToTranslatedJson(array(
'sun',
'mon',
'tue',
'wed',
'thu',
'fri',
'sat',
));
}
foreach ($data as $value) {
$value['data'] = array(array(0, (int) $value['data']));
$serie[] = $value;
}
/**
* Gets months ready for graphs
*
* @return string
*/
public function getMonths() {
return $this->convertToTranslatedJson(array(
'jan',
'feb',
'mar',
'apr',
'may',
'jun',
'jul',
'aug',
'sep',
'oct',
'nov',
'dec',
));
}
return json_encode($serie);
/**
* Translates array content
*
* @param array $data
* @return JSON object
*/
private function convertToTranslatedJson($data = array()) {
$translated = array_map(function($a) {
return _t('gen.date.' . $a);
}, $data);
return $translated;
}
}

View File

@@ -0,0 +1,67 @@
<?php
class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
/**
* Calculates the number of article per hour of the day per feed
*
* @param integer $feed id
* @return string
*/
public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
return $this->calculateEntryRepartitionPerFeedPerPeriod('hour', $feed);
}
/**
* Calculates the number of article per day of week per feed
*
* @param integer $feed id
* @return string
*/
public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
return $this->calculateEntryRepartitionPerFeedPerPeriod('day', $feed);
}
/**
* Calculates the number of article per month per feed
*
* @param integer $feed
* @return string
*/
public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
return $this->calculateEntryRepartitionPerFeedPerPeriod('month', $feed);
}
/**
* Calculates the number of article per period per feed
*
* @param string $period format string to use for grouping
* @param integer $feed id
* @return string
*/
protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
$restrict = '';
if ($feed) {
$restrict = "WHERE e.id_feed = {$feed}";
}
$sql = <<<SQL
SELECT extract( {$period} from to_timestamp(e.date)) AS period
, COUNT(1) AS count
FROM "{$this->prefix}entry" AS e
{$restrict}
GROUP BY period
ORDER BY period ASC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_NAMED);
foreach ($res as $value) {
$repartition[(int) $value['period']] = (int) $value['count'];
}
return $repartition;
}
}

View File

@@ -0,0 +1,36 @@
<?php
class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
protected function sqlFloor($s) {
return "CAST(($s) AS INT)";
}
protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
if ($feed) {
$restrict = "WHERE e.id_feed = {$feed}";
} else {
$restrict = '';
}
$sql = <<<SQL
SELECT strftime('{$period}', e.date, 'unixepoch') AS period
, COUNT(1) AS count
FROM `{$this->prefix}entry` AS e
{$restrict}
GROUP BY period
ORDER BY period ASC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_NAMED);
$repartition = array();
foreach ($res as $value) {
$repartition[(int) $value['period']] = (int) $value['count'];
}
return $repartition;
}
}

View File

@@ -31,7 +31,10 @@ class FreshRSS_Themes extends Minz_Model {
if (file_exists($json_filename)) {
$content = file_get_contents($json_filename);
$res = json_decode($content, true);
if ($res && isset($res['files']) && is_array($res['files'])) {
if ($res &&
!empty($res['name']) &&
isset($res['files']) &&
is_array($res['files'])) {
$res['id'] = $theme_id;
return $res;
}
@@ -70,6 +73,7 @@ class FreshRSS_Themes extends Minz_Model {
'add' => '✚',
'all' => '☰',
'bookmark' => '★',
'bookmark-add' => '✚',
'category' => '☷',
'category-white' => '☷',
'close' => '❌',
@@ -77,6 +81,9 @@ class FreshRSS_Themes extends Minz_Model {
'down' => '▽',
'favorite' => '★',
'help' => 'ⓘ',
'icon' => '⊚',
'import' => '⤓',
'key' => '⚿',
'link' => '↗',
'login' => '🔒',
'logout' => '🔓',
@@ -84,13 +91,18 @@ class FreshRSS_Themes extends Minz_Model {
'non-starred' => '☆',
'prev' => '⏪',
'read' => '☑',
'rss' => '☄',
'unread' => '☐',
'refresh' => '🔃', //↻
'search' => '🔍',
'share' => '♺',
'starred' => '★',
'stats' => '%',
'tag' => '⚐',
'up' => '△',
'view-normal' => '☰',
'view-global' => '☷',
'view-reader' => '☕',
);
if (!isset($alts[$name])) {
return '';

View File

@@ -1,36 +1,97 @@
<?php
class FreshRSS_UserDAO extends Minz_ModelPdo {
public function createUser($username) {
require_once(APP_PATH . '/sql.php');
$db = Minz_Configuration::dataBase();
public function createUser($username, $new_user_language, $insertDefaultFeeds = true) {
$db = FreshRSS_Context::$system_conf->db;
require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_');
$stm = $this->bd->prepare($sql, array(PDO::ATTR_EMULATE_PREPARES => true));
$values = array(
'catName' => Minz_Translate::t('default_category'),
);
if ($stm && $stm->execute($values)) {
$userPDO = new Minz_ModelPdo($username);
$currentLanguage = Minz_Translate::language();
try {
Minz_Translate::reset($new_user_language);
$ok = false;
$bd_prefix_user = $db['prefix'] . $username . '_';
if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL
$sql = sprintf(SQL_CREATE_TABLES, $bd_prefix_user, _t('gen.short.default_category'));
$stm = $userPDO->bd->prepare($sql);
$ok = $stm && $stm->execute();
} else { //E.g. SQLite
global $SQL_CREATE_TABLES;
if (is_array($SQL_CREATE_TABLES)) {
$ok = true;
foreach ($SQL_CREATE_TABLES as $instruction) {
$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
$stm = $userPDO->bd->prepare($sql);
$ok &= ($stm && $stm->execute());
}
}
}
if ($insertDefaultFeeds) {
if (defined('SQL_INSERT_FEEDS')) { //E.g. MySQL
$sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
$stm = $userPDO->bd->prepare($sql);
$ok &= $stm && $stm->execute();
} else { //E.g. SQLite
global $SQL_INSERT_FEEDS;
if (is_array($SQL_INSERT_FEEDS)) {
foreach ($SQL_INSERT_FEEDS as $instruction) {
$sql = sprintf($instruction, $bd_prefix_user);
$stm = $userPDO->bd->prepare($sql);
$ok &= ($stm && $stm->execute());
}
}
}
}
} catch (Exception $e) {
Minz_Log::error('Error while creating user: ' . $e->getMessage());
}
Minz_Translate::reset($currentLanguage);
if ($ok) {
return true;
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
$info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error: ' . $info[2]);
return false;
}
}
public function deleteUser($username) {
require_once(APP_PATH . '/sql.php');
$db = Minz_Configuration::dataBase();
$db = FreshRSS_Context::$system_conf->db;
require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute()) {
return true;
if ($db['type'] === 'sqlite') {
return unlink(join_path(DATA_PATH, 'users', $username, 'db.sqlite'));
} else {
$info = $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
return false;
$userPDO = new Minz_ModelPdo($username);
$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
$stm = $userPDO->bd->prepare($sql);
if ($stm && $stm->execute()) {
return true;
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error : ' . $info[2]);
return false;
}
}
}
public static function exist($username) {
return is_dir(join_path(DATA_PATH , 'users', $username));
}
public static function touch($username = '') {
if (($username == '') || (!ctype_alnum($username))) {
$username = Minz_Session::param('currentUser', '_');
}
return touch(join_path(DATA_PATH , 'users', $username, 'config.php'));
}
public static function mtime($username) {
return @filemtime(join_path(DATA_PATH , 'users', $username, 'config.php'));
}
}

226
app/Models/UserQuery.php Normal file
View File

@@ -0,0 +1,226 @@
<?php
/**
* Contains the description of a user query
*
* It allows to extract the meaningful bits of the query to be manipulated in an
* easy way.
*/
class FreshRSS_UserQuery {
private $deprecated = false;
private $get;
private $get_name;
private $get_type;
private $name;
private $order;
private $search;
private $state;
private $url;
private $feed_dao;
private $category_dao;
/**
* @param array $query
* @param FreshRSS_Searchable $feed_dao
* @param FreshRSS_Searchable $category_dao
*/
public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null) {
$this->category_dao = $category_dao;
$this->feed_dao = $feed_dao;
if (isset($query['get'])) {
$this->parseGet($query['get']);
}
if (isset($query['name'])) {
$this->name = $query['name'];
}
if (isset($query['order'])) {
$this->order = $query['order'];
}
if (!isset($query['search'])) {
$query['search'] = '';
}
// linked to deeply with the search object, need to use dependency injection
$this->search = new FreshRSS_Search($query['search']);
if (isset($query['state'])) {
$this->state = $query['state'];
}
if (isset($query['url'])) {
$this->url = $query['url'];
}
}
/**
* Convert the current object to an array.
*
* @return array
*/
public function toArray() {
return array_filter(array(
'get' => $this->get,
'name' => $this->name,
'order' => $this->order,
'search' => $this->search->__toString(),
'state' => $this->state,
'url' => $this->url,
));
}
/**
* Parse the get parameter in the query string to extract its name and
* type
*
* @param string $get
*/
private function parseGet($get) {
$this->get = $get;
if (preg_match('/(?P<type>[acfs])(_(?P<id>\d+))?/', $get, $matches)) {
switch ($matches['type']) {
case 'a':
$this->parseAll();
break;
case 'c':
$this->parseCategory($matches['id']);
break;
case 'f':
$this->parseFeed($matches['id']);
break;
case 's':
$this->parseFavorite();
break;
}
}
}
/**
* Parse the query string when it is an "all" query
*/
private function parseAll() {
$this->get_name = 'all';
$this->get_type = 'all';
}
/**
* Parse the query string when it is a "category" query
*
* @param integer $id
* @throws FreshRSS_DAO_Exception
*/
private function parseCategory($id) {
if (is_null($this->category_dao)) {
throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery');
}
$category = $this->category_dao->searchById($id);
if ($category) {
$this->get_name = $category->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'category';
}
/**
* Parse the query string when it is a "feed" query
*
* @param integer $id
* @throws FreshRSS_DAO_Exception
*/
private function parseFeed($id) {
if (is_null($this->feed_dao)) {
throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery');
}
$feed = $this->feed_dao->searchById($id);
if ($feed) {
$this->get_name = $feed->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'feed';
}
/**
* Parse the query string when it is a "favorite" query
*/
private function parseFavorite() {
$this->get_name = 'favorite';
$this->get_type = 'favorite';
}
/**
* Check if the current user query is deprecated.
* It is deprecated if the category or the feed used in the query are
* not existing.
*
* @return boolean
*/
public function isDeprecated() {
return $this->deprecated;
}
/**
* Check if the user query has parameters.
* If the type is 'all', it is considered equal to no parameters
*
* @return boolean
*/
public function hasParameters() {
if ($this->get_type === 'all') {
return false;
}
if ($this->hasSearch()) {
return true;
}
if ($this->state) {
return true;
}
if ($this->order) {
return true;
}
if ($this->get) {
return true;
}
return false;
}
/**
* Check if there is a search in the search object
*
* @return boolean
*/
public function hasSearch() {
return $this->search->getRawInput() != "";
}
public function getGet() {
return $this->get;
}
public function getGetName() {
return $this->get_name;
}
public function getGetType() {
return $this->get_type;
}
public function getName() {
return $this->name;
}
public function getOrder() {
return $this->order;
}
public function getSearch() {
return $this->search;
}
public function getState() {
return $this->state;
}
public function getUrl() {
return $this->url;
}
}

View File

@@ -0,0 +1,90 @@
<?php
define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
define('SQL_CREATE_TABLES', '
CREATE TABLE IF NOT EXISTS `%1$scategory` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
`name` varchar(191) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`name`) -- v0.7
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `%1$sfeed` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
`url` varchar(511) CHARACTER SET latin1 NOT NULL,
`category` SMALLINT DEFAULT 0, -- v0.7
`name` varchar(191) NOT NULL,
`website` varchar(255) CHARACTER SET latin1,
`description` text,
`lastUpdate` int(11) DEFAULT 0, -- Until year 2038
`priority` tinyint(2) NOT NULL DEFAULT 10,
`pathEntries` varchar(511) DEFAULT NULL,
`httpAuth` varchar(511) DEFAULT NULL,
`error` boolean DEFAULT 0,
`keep_history` MEDIUMINT NOT NULL DEFAULT -2, -- v0.7
`ttl` INT NOT NULL DEFAULT -2, -- v0.7.3
`cache_nbEntries` int DEFAULT 0, -- v0.7
`cache_nbUnreads` int DEFAULT 0, -- v0.7
PRIMARY KEY (`id`),
FOREIGN KEY (`category`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
UNIQUE KEY (`url`), -- v0.7
INDEX (`name`), -- v0.7
INDEX (`priority`), -- v0.7
INDEX (`keep_history`) -- v0.7
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `%1$sentry` (
`id` bigint NOT NULL, -- v0.7
`guid` varchar(760) CHARACTER SET latin1 NOT NULL, -- Maximum for UNIQUE is 767B
`title` varchar(255) NOT NULL,
`author` varchar(255),
`content_bin` blob, -- v0.7
`link` varchar(1023) CHARACTER SET latin1 NOT NULL,
`date` int(11), -- Until year 2038
`lastSeen` INT(11) DEFAULT 0, -- v1.1.1, Until year 2038
`hash` BINARY(16), -- v1.1.1
`is_read` boolean NOT NULL DEFAULT 0,
`is_favorite` boolean NOT NULL DEFAULT 0,
`id_feed` SMALLINT, -- v0.7
`tags` varchar(1023),
PRIMARY KEY (`id`),
FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE KEY (`id_feed`,`guid`), -- v0.7
INDEX (`is_favorite`), -- v0.7
INDEX (`is_read`), -- v0.7
INDEX `entry_lastSeen_index` (`lastSeen`) -- v1.1.1
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
');
define('SQL_INSERT_FEEDS', '
INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);
');
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentry`, `%1$sfeed`, `%1$scategory`');
define('SQL_UPDATE_UTF8MB4', '
ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `%1$scategory` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
UPDATE `%1$scategory` SET name=SUBSTRING(name,1,190) WHERE LENGTH(name) > 191;
ALTER TABLE `%1$scategory` MODIFY `name` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
OPTIMIZE TABLE `%1$scategory`;
ALTER TABLE `%1$sfeed` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
UPDATE `%1$sfeed` SET name=SUBSTRING(name,1,190) WHERE LENGTH(name) > 191;
ALTER TABLE `%1$sfeed` MODIFY `name` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
ALTER TABLE `%1$sfeed` MODIFY `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
OPTIMIZE TABLE `%1$sfeed`;
ALTER TABLE `%1$sentry` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `%1$sentry` MODIFY `title` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
ALTER TABLE `%1$sentry` MODIFY `author` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `%1$sentry` MODIFY `tags` VARCHAR(1023) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
OPTIMIZE TABLE `%1$sentry`;
');

View File

@@ -0,0 +1,63 @@
<?php
define('SQL_CREATE_DB', 'CREATE DATABASE %1$s ENCODING \'UTF8\';');
global $SQL_CREATE_TABLES;
$SQL_CREATE_TABLES = array(
'CREATE TABLE IF NOT EXISTS "%1$scategory" (
"id" SERIAL PRIMARY KEY,
"name" VARCHAR(255) UNIQUE NOT NULL
);',
'CREATE TABLE IF NOT EXISTS "%1$sfeed" (
"id" SERIAL PRIMARY KEY,
"url" varchar(511) UNIQUE NOT NULL,
"category" SMALLINT DEFAULT 0,
"name" VARCHAR(255) NOT NULL,
"website" VARCHAR(255),
"description" text,
"lastUpdate" INT DEFAULT 0,
"priority" SMALLINT NOT NULL DEFAULT 10,
"pathEntries" VARCHAR(511) DEFAULT NULL,
"httpAuth" VARCHAR(511) DEFAULT NULL,
"error" smallint DEFAULT 0,
"keep_history" INT NOT NULL DEFAULT -2,
"ttl" INT NOT NULL DEFAULT -2,
"cache_nbEntries" INT DEFAULT 0,
"cache_nbUnreads" INT DEFAULT 0,
FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);',
'CREATE INDEX %1$sname_index ON "%1$sfeed" ("name");',
'CREATE INDEX %1$spriority_index ON "%1$sfeed" ("priority");',
'CREATE INDEX %1$skeep_history_index ON "%1$sfeed" ("keep_history");',
'CREATE TABLE IF NOT EXISTS "%1$sentry" (
"id" BIGINT NOT NULL PRIMARY KEY,
"guid" VARCHAR(760) UNIQUE NOT NULL,
"title" VARCHAR(255) NOT NULL,
"author" VARCHAR(255),
"content" TEXT,
"link" VARCHAR(1023) NOT NULL,
"date" INT,
"lastSeen" INT DEFAULT 0,
"hash" BYTEA,
"is_read" SMALLINT NOT NULL DEFAULT 0,
"is_favorite" SMALLINT NOT NULL DEFAULT 0,
"id_feed" SMALLINT,
"tags" VARCHAR(1023),
FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE ("id_feed","guid")
);',
'CREATE INDEX %1$sis_favorite_index ON "%1$sentry" ("is_favorite");',
'CREATE INDEX %1$sis_read_index ON "%1$sentry" ("is_read");',
'CREATE INDEX %1$sentry_lastSeen_index ON "%1$sentry" ("lastSeen");',
'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);',
);
global $SQL_INSERT_FEEDS;
$SQL_INSERT_FEEDS = array(
'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');',
'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');',
);
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentry", "%1$sfeed", "%1$scategory"');

View File

@@ -0,0 +1,66 @@
<?php
global $SQL_CREATE_TABLES;
$SQL_CREATE_TABLES = array(
'CREATE TABLE IF NOT EXISTS `category` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` varchar(255) NOT NULL,
UNIQUE (`name`)
);',
'CREATE TABLE IF NOT EXISTS `feed` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`url` varchar(511) NOT NULL,
`category` SMALLINT DEFAULT 0,
`name` varchar(255) NOT NULL,
`website` varchar(255),
`description` text,
`lastUpdate` int(11) DEFAULT 0, -- Until year 2038
`priority` tinyint(2) NOT NULL DEFAULT 10,
`pathEntries` varchar(511) DEFAULT NULL,
`httpAuth` varchar(511) DEFAULT NULL,
`error` boolean DEFAULT 0,
`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
`ttl` INT NOT NULL DEFAULT -2,
`cache_nbEntries` int DEFAULT 0,
`cache_nbUnreads` int DEFAULT 0,
FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
UNIQUE (`url`)
);',
'CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);',
'CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);',
'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
'CREATE TABLE IF NOT EXISTS `entry` (
`id` bigint NOT NULL,
`guid` varchar(760) NOT NULL,
`title` varchar(255) NOT NULL,
`author` varchar(255),
`content` text,
`link` varchar(1023) NOT NULL,
`date` int(11), -- Until year 2038
`lastSeen` INT(11) DEFAULT 0, -- v1.1.1, Until year 2038
`hash` BINARY(16), -- v1.1.1
`is_read` boolean NOT NULL DEFAULT 0,
`is_favorite` boolean NOT NULL DEFAULT 0,
`id_feed` SMALLINT,
`tags` varchar(1023),
PRIMARY KEY (`id`),
FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE (`id_feed`,`guid`)
);',
'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `entry`(`is_favorite`);',
'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `entry`(`is_read`);',
'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `entry`(`lastSeen`);', //v1.1.1
'INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "%2$s");',
);
global $SQL_INSERT_FEEDS;
$SQL_INSERT_FEEDS = array(
'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);',
'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);',
);
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS entry, feed, category');

View File

@@ -7,49 +7,81 @@ ob_implicit_flush(false);
ob_start();
echo 'Results: ', "\n"; //Buffered
Minz_Configuration::init();
if (defined('STDOUT')) {
$begin_date = date_create('now');
fwrite(STDOUT, 'Starting feed actualization at ' . $begin_date->format('c') . "\n"); //Unbuffered
}
// Set the header params ($_GET) to call the FRSS application.
$_GET['c'] = 'feed';
$_GET['a'] = 'actualize';
$_GET['ajax'] = 1;
$_GET['force'] = true;
$_SERVER['HTTP_HOST'] = '';
$log_file = join_path(USERS_PATH, '_', 'log.txt');
$app = new FreshRSS();
$system_conf = Minz_Configuration::get('system');
$system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!)
FreshRSS_Context::$isCli = true;
// Create the list of users to actualize.
// Users are processed in a random order but always start with admin
$users = listUsers();
shuffle($users); //Process users in random order
array_unshift($users, Minz_Configuration::defaultUser()); //But always start with admin
$users = array_unique($users);
shuffle($users);
if ($system_conf->default_user !== '') {
array_unshift($users, $system_conf->default_user);
$users = array_unique($users);
}
foreach ($users as $myUser) {
syslog(LOG_INFO, 'FreshRSS actualize ' . $myUser);
if (defined('STDOUT')) {
fwrite(STDOUT, 'Actualize ' . $myUser . "...\n"); //Unbuffered
$limits = $system_conf->limits;
$min_last_activity = time() - $limits['max_inactivity'];
foreach ($users as $user) {
if (($user !== $system_conf->default_user) &&
(FreshRSS_UserDAO::mtime($user) < $min_last_activity)) {
Minz_Log::notice('FreshRSS skip inactive user ' . $user, $log_file);
if (defined('STDOUT')) {
fwrite(STDOUT, 'FreshRSS skip inactive user ' . $user . "\n"); //Unbuffered
}
continue;
}
echo $myUser, ' '; //Buffered
Minz_Log::notice('FreshRSS actualize ' . $user, $log_file);
if (defined('STDOUT')) {
fwrite(STDOUT, 'Actualize ' . $user . "...\n"); //Unbuffered
}
echo $user, ' '; //Buffered
$_GET['c'] = 'feed';
$_GET['a'] = 'actualize';
$_GET['ajax'] = 1;
$_GET['force'] = true;
$_SERVER['HTTP_HOST'] = '';
$freshRSS = new FreshRSS();
$freshRSS->_useOb(false);
Minz_Session::_param('currentUser', $user);
new Minz_ModelPdo($user); //TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init
FreshRSS_Auth::giveAccess();
$app->init();
$app->run();
Minz_Configuration::_authType('none');
Minz_Session::init('FreshRSS');
Minz_Session::_param('currentUser', $myUser);
$freshRSS->init();
$freshRSS->run();
if (!invalidateHttpCache()) {
syslog(LOG_NOTICE, 'FreshRSS write access problem in ' . LOG_PATH . '/*.log!');
Minz_Log::notice('FreshRSS write access problem in ' . join_path(USERS_PATH, $user, 'log.txt'),
$log_file);
if (defined('STDERR')) {
fwrite(STDERR, 'Write access problem in ' . LOG_PATH . '/*.log!' . "\n");
fwrite(STDERR, 'Write access problem in ' . join_path(USERS_PATH, $user, 'log.txt') . "\n");
}
}
Minz_Session::unset_session(true);
Minz_ModelPdo::clean();
}
syslog(LOG_INFO, 'FreshRSS actualize done.');
Minz_Log::notice('FreshRSS actualize done.', $log_file);
if (defined('STDOUT')) {
fwrite(STDOUT, 'Done.' . "\n");
$end_date = date_create('now');
$duration = date_diff($end_date, $begin_date);
fwrite(STDOUT, 'Ending feed actualization at ' . $end_date->format('c') . "\n"); //Unbuffered
fwrite(STDOUT, 'Feed actualizations took ' . $duration->format('%a day(s), %h hour(s), %i minute(s) and %s seconds') . ' for ' . count($users) . " users\n"); //Unbuffered
}
echo 'End.', "\n";
ob_end_flush();

181
app/i18n/cz/admin.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
return array(
'auth' => array(
'allow_anonymous' => 'Umožnit anonymně číst články výchozího uživatele (%s)',
'allow_anonymous_refresh' => 'Umožnit anonymní obnovení článků',
'api_enabled' => 'Povolit přístup k <abbr>API</abbr> <small>(vyžadováno mobilními aplikacemi)</small>',
'form' => 'Webový formulář (tradiční, vyžaduje JavaScript)',
'http' => 'HTTP (pro pokročilé uživatele s HTTPS)',
'none' => 'Žádný (nebezpečné)',
'title' => 'Přihlášení',
'title_reset' => 'Reset přihlášení',
'token' => 'Authentizační token',
'token_help' => 'Umožňuje přístup k RSS kanálu článků výchozího uživatele bez přihlášení:',
'type' => 'Způsob přihlášení',
'unsafe_autologin' => 'Povolit nebezpečné automatické přihlášení přes: ',
),
'check_install' => array(
'cache' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/cache</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře cache jsou v pořádku.',
),
'categories' => array(
'nok' => 'Tabulka kategorií je nastavena špatně.',
'ok' => 'Tabulka kategorií je v pořádku.',
),
'connection' => array(
'nok' => 'Nelze navázat spojení s databází.',
'ok' => 'Připojení k databázi je v pořádku.',
),
'ctype' => array(
'nok' => 'Nemáte požadovanou knihovnu pro ověřování znaků (php-ctype).',
'ok' => 'Máte požadovanou knihovnu pro ověřování znaků (ctype).',
),
'curl' => array(
'nok' => 'Nemáte cURL (balíček php-curl).',
'ok' => 'Máte rozšíření cURL.',
),
'data' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře data jsou v pořádku.',
),
'database' => 'Instalace databáze',
'dom' => array(
'nok' => 'Nemáte požadovanou knihovnu pro procházení DOM (balíček php-xml).',
'ok' => 'Máte požadovanou knihovnu pro procházení DOM.',
),
'entries' => array(
'nok' => 'Tabulka článků je nastavena špatně.',
'ok' => 'Tabulka kategorií je v pořádku.',
),
'favicons' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/favicons</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře favicons jsou v pořádku.',
),
'feeds' => array(
'nok' => 'Tabulka kanálů je nastavena špatně.',
'ok' => 'Tabulka kanálů je v pořádku.',
),
'fileinfo' => array(
'nok' => 'Nemáte PHP fileinfo (balíček fileinfo).',
'ok' => 'Máte rozšíření fileinfo.',
),
'files' => 'Instalace souborů',
'json' => array(
'nok' => 'Nemáte JSON (balíček php5-json).',
'ok' => 'Máte rozšíření JSON.',
),
'minz' => array(
'nok' => 'Nemáte framework Minz.',
'ok' => 'Máte framework Minz.',
),
'pcre' => array(
'nok' => 'Nemáte požadovanou knihovnu pro regulární výrazy (php-pcre).',
'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
),
'pdo' => array(
'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP instalace',
'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.',
'ok' => 'Vaše verze PHP je %s a je kompatibilní s FreshRSS.',
),
'tables' => array(
'nok' => 'V databázi chybí jedna nevo více tabulek.',
'ok' => 'V databázi jsou všechny tabulky.',
),
'title' => 'Kontrola instalace',
'tokens' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/tokens</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře tokens jsou v pořádku.',
),
'users' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/users</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře users jsou v pořádku.',
),
'zip' => array(
'nok' => 'Nemáte rozšíření ZIP (balíček php-zip).',
'ok' => 'Máte rozšíření ZIP.',
),
),
'extensions' => array(
'disabled' => 'Vypnuto',
'empty_list' => 'Není naistalováno žádné rozšíření',
'enabled' => 'Zapnuto',
'no_configure_view' => 'Toto rozšíření nemá žádné možnosti nastavení.',
'system' => array(
'_' => 'Systémová rozšíření',
'no_rights' => 'Systémová rozšíření (na ně nemáte oprávnění)',
),
'title' => 'Rozšíření',
'user' => 'Uživatelská rozšíření',
),
'stats' => array(
'_' => 'Statistika',
'all_feeds' => 'Všechny kanály',
'category' => 'Kategorie',
'entry_count' => 'Počet článků',
'entry_per_category' => 'Článků na kategorii',
'entry_per_day' => 'Článků za den (posledních 30 dní)',
'entry_per_day_of_week' => 'Za den v týdnu (průměr: %.2f zprávy)',
'entry_per_hour' => 'Za hodinu (průměr: %.2f zprávy)',
'entry_per_month' => 'Za měsíc (průměr: %.2f zprávy)',
'entry_repartition' => 'Rozdělení článků',
'feed' => 'Kanál',
'feed_per_category' => 'Článků na kategorii',
'idle' => 'Neaktivní kanály',
'main' => 'Přehled',
'main_stream' => 'Všechny kanály',
'menu' => array(
'idle' => 'Neaktivní kanály',
'main' => 'Přehled',
'repartition' => 'Rozdělení článků',
),
'no_idle' => 'Žádné neaktivní kanály!',
'number_entries' => '%d článků',
'percent_of_total' => '%% ze všech',
'repartition' => 'Rozdělení článků',
'status_favorites' => 'Oblíbené',
'status_read' => 'Přečtené',
'status_total' => 'Celkem',
'status_unread' => 'Nepřečtené',
'title' => 'Statistika',
'top_feed' => 'Top ten kanálů',
),
'system' => array(
'_' => 'System configuration', // @todo translate
'auto-update-url' => 'Auto-update server URL', // @todo translate
'instance-name' => 'Instance name', // @todo translate
'max-categories' => 'Categories per user limit', // @todo translate
'max-feeds' => 'Feeds per user limit', // @todo translate
'registration' => array(
'help' => '0 znamená žádná omezení účtu',
'number' => 'Maximální počet účtů',
),
),
'update' => array(
'_' => 'Aktualizace systému',
'apply' => 'Použít',
'check' => 'Zkontrolovat aktualizace',
'current_version' => 'Vaše instalace FreshRSS je verze %s.',
'last' => 'Poslední kontrola: %s',
'none' => 'Žádné nové aktualizace',
'title' => 'Aktualizovat systém',
),
'user' => array(
'articles_and_size' => '%s článků (%s)',
'create' => 'Vytvořit nového uživatele',
'language' => 'Jazyk',
'number' => 'Zatím je vytvořen %d účet',
'numbers' => 'Zatím je vytvořeno %d účtů',
'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
'password_format' => 'Alespoň 7 znaků',
'title' => 'Správa uživatelů',
'user_list' => 'Seznam uživatelů',
'username' => 'Přihlašovací jméno',
'users' => 'Uživatelé',
),
);

173
app/i18n/cz/conf.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
return array(
'archiving' => array(
'_' => 'Archivace',
'advanced' => 'Pokročilé',
'delete_after' => 'Smazat články starší než',
'help' => 'Více možností je dostupných v nastavení jednotlivých kanálů',
'keep_history_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu',
'optimize' => 'Optimalizovat databázi',
'optimize_help' => 'Občasná údržba zmenší velikost databáze',
'purge_now' => 'Vyčistit nyní',
'title' => 'Archivace',
'ttl' => 'Neaktualizovat častěji než',
),
'display' => array(
'_' => 'Zobrazení',
'icon' => array(
'bottom_line' => 'Spodní řádek',
'entry' => 'Ikony článků',
'publication_date' => 'Datum vydání',
'related_tags' => 'Související tagy',
'sharing' => 'Sdílení',
'top_line' => 'Horní řádek',
),
'language' => 'Jazyk',
'notif_html5' => array(
'seconds' => 'sekund (0 znamená žádný timeout)',
'timeout' => 'Timeout HTML5 notifikací',
),
'theme' => 'Vzhled',
'title' => 'Zobrazení',
'width' => array(
'content' => 'Šířka obsahu',
'large' => 'Velká',
'medium' => 'Střední',
'no_limit' => 'Bez limitu',
'thin' => 'Tenká',
),
),
'query' => array(
'_' => 'Uživatelské dotazy',
'deprecated' => 'Tento dotaz již není platný. Odkazovaná kategorie nebo kanál byly smazány.',
'filter' => 'Filtr aplikován:',
'get_all' => 'Zobrazit všechny články',
'get_category' => 'Zobrazit "%s" kategorii',
'get_favorite' => 'Zobrazit oblíbené články',
'get_feed' => 'Zobrazit "%s" článkek',
'no_filter' => 'Zrušit filtr',
'none' => 'Ještě jste nevytvořil žádný uživatelský dotaz.',
'number' => 'Dotaz n°%d',
'order_asc' => 'Zobrazit nejdříve nejstarší články',
'order_desc' => 'Zobrazit nejdříve nejnovější články',
'search' => 'Hledat "%s"',
'state_0' => 'Zobrazit všechny články',
'state_1' => 'Zobrazit přečtené články',
'state_2' => 'Zobrazit nepřečtené články',
'state_3' => 'Zobrazit všechny články',
'state_4' => 'Zobrazit oblíbené články',
'state_5' => 'Zobrazit oblíbené přečtené články',
'state_6' => 'Zobrazit oblíbené nepřečtené články',
'state_7' => 'Zobrazit oblíbené články',
'state_8' => 'Zobrazit všechny články vyjma oblíbených',
'state_9' => 'Zobrazit všechny přečtené články vyjma oblíbených',
'state_10' => 'Zobrazit všechny nepřečtené články vyjma oblíbených',
'state_11' => 'Zobrazit všechny články vyjma oblíbených',
'state_12' => 'Zobrazit všechny články',
'state_13' => 'Zobrazit přečtené články',
'state_14' => 'Zobrazit nepřečtené články',
'state_15' => 'Zobrazit všechny články',
'title' => 'Uživatelské dotazy',
),
'profile' => array(
'_' => 'Správa profilu',
'delete' => array(
'_' => 'Smazání účtu',
'warn' => 'Váš účet bude smazán spolu se všemi souvisejícími daty',
),
'password_api' => 'Password API<br /><small>(tzn. pro mobilní aplikace)</small>',
'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
'password_format' => 'Alespoň 7 znaků',
'title' => 'Profil',
),
'reading' => array(
'_' => 'Čtení',
'after_onread' => 'Po “označit vše jako přečtené”,',
'articles_per_page' => 'Počet článků na stranu',
'auto_load_more' => 'Načítat další články dole na stránce',
'auto_remove_article' => 'Po přečtení články schovat',
'mark_updated_article_unread' => 'Označte aktualizované položky jako nepřečtené',
'confirm_enabled' => 'Vyžadovat potvrzení pro akci “označit vše jako přečtené”',
'display_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené',
'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené',
'hide_read_feeds' => 'Schovat kategorie a kanály s nulovým počtem nepřečtených článků (nefunguje s nastavením “Zobrazit všechny články”)',
'img_with_lazyload' => 'Použít "lazy load" mód pro načítaní obrázků',
'jump_next' => 'skočit na další nepřečtený (kanál nebo kategorii)',
'number_divided_when_reader' => 'V režimu “Čtení” děleno dvěma.',
'read' => array(
'article_open_on_website' => 'když je otevřen původní web s článkem',
'article_viewed' => 'během čtení článku',
'scroll' => 'během skrolování',
'upon_reception' => 'po načtení článku',
'when' => 'Označit článek jako přečtený…',
),
'show' => array(
'_' => 'Počet zobrazených článků',
'adaptive' => 'Vyberte zobrazení',
'all_articles' => 'Zobrazit všechny články',
'unread' => 'Zobrazit jen nepřečtené',
),
'sort' => array(
'_' => 'Řazení',
'newer_first' => 'Nejdříve nejnovější',
'older_first' => 'Nejdříve nejstarší',
),
'sticky_post' => 'Při otevření posunout článek nahoru',
'title' => 'Čtení',
'view' => array(
'default' => 'Výchozí',
'global' => 'Přehled',
'normal' => 'Normální',
'reader' => 'Čtení',
),
),
'sharing' => array(
'_' => 'Sdílení',
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Více informací',
'print' => 'Tisk',
'shaarli' => 'Shaarli',
'share_name' => 'Jméno pro zobrazení',
'share_url' => 'Jakou URL použít pro sdílení',
'title' => 'Sdílení',
'twitter' => 'Twitter',
'wallabag' => 'wallabag',
),
'shortcut' => array(
'_' => 'Zkratky',
'article_action' => 'Články - akce',
'auto_share' => 'Sdílet',
'auto_share_help' => 'Je-li nastavena pouze jedna možnost sdílení, bude použita. Další možnosti jsou dostupné pomocí jejich čísla.',
'close_dropdown' => 'Zavřít menu',
'collapse_article' => 'Srolovat',
'first_article' => 'Skočit na první článek',
'focus_search' => 'Hledání',
'help' => 'Zobrazit documentaci',
'javascript' => 'Pro použití zkratek musí být povolen JavaScript',
'last_article' => 'Skočit na poslední článek',
'load_more' => 'Načíst více článků',
'mark_read' => 'Označit jako přečtené',
'mark_favorite' => 'Označit jako oblíbené',
'navigation' => 'Navigace',
'navigation_help' => 'Pomocí přepínače "Shift" fungují navigační zkratky v rámci kanálů.<br/>Pomocí přepínače "Alt" fungují v rámci kategorií.',
'next_article' => 'Skočit na další článek',
'other_action' => 'Ostatní akce',
'previous_article' => 'Skočit na předchozí článek',
'see_on_website' => 'Navštívit původní webovou stránku',
'shift_for_all_read' => '+ <code>shift</code> označí vše jako přečtené',
'title' => 'Zkratky',
'user_filter' => 'Aplikovat uživatelské filtry',
'user_filter_help' => 'Je-li nastaven pouze jeden filtr, bude použit. Další filtry jsou dostupné pomocí jejich čísla.',
),
'user' => array(
'articles_and_size' => '%s článků (%s)',
'current' => 'Aktuální uživatel',
'is_admin' => 'je administrátor',
'users' => 'Uživatelé',
),
);

109
app/i18n/cz/feedback.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
return array(
'admin' => array(
'optimization_complete' => 'Optimalizace dokončena',
),
'access' => array(
'denied' => 'Nemáte oprávnění přistupovat na tuto stránku',
'not_found' => 'Tato stránka neexistuje',
),
'auth' => array(
'form' => array(
'not_set' => 'Nastal problém s konfigurací přihlašovacího systému. Zkuste to prosím později.',
'set' => 'Webový formulář je nyní výchozí přihlašovací systém.',
),
'login' => array(
'invalid' => 'Login není platný',
'success' => 'Jste přihlášen',
),
'logout' => array(
'success' => 'Jste odhlášen',
),
'no_password_set' => 'Heslo administrátora nebylo nastaveno. Tato funkce není k dispozici.',
),
'conf' => array(
'error' => 'Během ukládání nastavení došlo k chybě',
'query_created' => 'Dotaz "%s" byl vytvořen.',
'shortcuts_updated' => 'Zkratky byly aktualizovány',
'updated' => 'Nastavení bylo aktualizováno',
),
'extensions' => array(
'already_enabled' => '%s je již zapnut',
'disable' => array(
'ko' => '%s nelze vypnout. Pro více detailů <a href="%s">zkontrolujte logy FressRSS</a>.',
'ok' => '%s je nyní vypnut',
),
'enable' => array(
'ko' => '%s nelze zapnout. Pro více detailů <a href="%s">zkontrolujte logy FressRSS</a>.',
'ok' => '%s je nyní zapnut',
),
'no_access' => 'Nemáte přístup k %s',
'not_enabled' => '%s není ještě zapnut',
'not_found' => '%s neexistuje',
),
'import_export' => array(
'export_no_zip_extension' => 'Na serveru není naistalována podpora ZIP. Zkuste prosím exportovat soubory jeden po druhém.',
'feeds_imported' => 'Vaše kanály byly naimportovány a nyní budou aktualizovány',
'feeds_imported_with_errors' => 'Vaše kanály byly naimportovány, došlo ale k nějakým chybám',
'file_cannot_be_uploaded' => 'Soubor nelze nahrát!',
'no_zip_extension' => 'Na serveru není naistalována podpora ZIP.',
'zip_error' => 'Během importu ZIP souboru došlo k chybě.',
),
'sub' => array(
'actualize' => 'Aktualizovat',
'category' => array(
'created' => 'Kategorie %s byla vytvořena.',
'deleted' => 'Kategorie byla smazána.',
'emptied' => 'Kategorie byla vyprázdněna',
'error' => 'Kategorii nelze aktualizovat',
'name_exists' => 'Název kategorie již existuje.',
'no_id' => 'Musíte upřesnit id kategorie.',
'no_name' => 'Název kategorie nemůže být prázdný.',
'not_delete_default' => 'Nelze smazat výchozí kategorii!',
'not_exist' => 'Tato kategorie neexistuje!',
'over_max' => 'Dosáhl jste maximálního počtu kategorií (%d)',
'updated' => 'Kategorie byla aktualizována.',
),
'feed' => array(
'actualized' => '<em>%s</em> bylo aktualizováno',
'actualizeds' => 'RSS kanály byly aktualizovány',
'added' => 'RSS kanál <em>%s</em> byl přidán',
'already_subscribed' => 'Již jste přihlášen k odběru <em>%s</em>',
'deleted' => 'Kanál byl smazán',
'error' => 'Kanál nelze aktualizovat',
'internal_problem' => 'RSS kanál nelze přidat. Pro detaily <a href="%s">zkontrolujte logy FressRSS</a>.',
'invalid_url' => 'URL <em>%s</em> není platné',
'marked_read' => 'Kanály byly označeny jako přečtené',
'n_actualized' => '%d kanálů bylo aktualizováno',
'n_entries_deleted' => '%d článků bylo smazáno',
'no_refresh' => 'Nelze obnovit žádné kanály…',
'not_added' => '<em>%s</em> nemůže být přidán',
'over_max' => 'Dosáhl jste maximálního počtu kanálů (%d)',
'updated' => 'Kanál byl aktualizován',
),
'purge_completed' => 'Vyprázdněno (smazáno %d článků)',
),
'update' => array(
'can_apply' => 'FreshRSS bude nyní upgradováno na <strong>verzi %s</strong>.',
'error' => 'Během upgrade došlo k chybě: %s',
'file_is_nok' => 'Zkontrolujte oprávnění adresáře <em>%s</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'finished' => 'Upgrade hotov!',
'none' => 'Novější verze není k dispozici',
'server_not_found' => 'Nelze nalézt server s instalačním souborem. [%s]',
),
'user' => array(
'created' => array(
'_' => 'Uživatel %s byl vytvořen',
'error' => 'Uživatele %s nelze vytvořit',
),
'deleted' => array(
'_' => 'Uživatel %s byl smazán',
'error' => 'Uživatele %s nelze smazat',
),
),
'profile' => array(
'error' => 'Váš profil nelze změnit',
'updated' => 'Váš profil byl změněn',
),
);

181
app/i18n/cz/gen.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
return array(
'action' => array(
'actualize' => 'Aktualizovat',
'back_to_rss_feeds' => '← Zpět na seznam RSS kanálů',
'cancel' => 'Zrušit',
'create' => 'Vytvořit',
'disable' => 'Zakázat',
'empty' => 'Vyprázdnit',
'enable' => 'Povolit',
'export' => 'Export',
'filter' => 'Filtrovat',
'import' => 'Import',
'manage' => 'Spravovat',
'mark_read' => 'Označit jako přečtené',
'mark_favorite' => 'Označit jako oblíbené',
'remove' => 'Odstranit',
'see_website' => 'Navštívit WWW stránku',
'submit' => 'Odeslat',
'truncate' => 'Smazat všechny články',
),
'auth' => array(
'email' => 'Email',
'keep_logged_in' => 'Zapamatovat přihlášení <small>(%s dny)</small>',
'login' => 'Login',
'logout' => 'Odhlášení',
'password' => array(
'_' => 'Heslo',
'format' => '<small>Alespoň 7 znaků</small>',
),
'registration' => array(
'_' => 'Nový účet',
'ask' => 'Vytvořit účet?',
'title' => 'Vytvoření účtu',
),
'reset' => 'Reset přihlášení',
'username' => array(
'_' => 'Uživatel',
'admin' => 'Název administrátorského účtu',
'format' => '<small>maximálně 16 alfanumerických znaků</small>',
),
),
'date' => array(
'Apr' => '\\D\\u\\b\\e\\n',
'Aug' => '\\S\\r\\p\\e\\n',
'Dec' => '\\P\\r\\o\\s\\i\\n\\e\\c',
'Feb' => '\\Ú\\n\\o\\r',
'Jan' => '\\L\\e\\d\\e\\n',
'Jul' => '\\Č\\e\\r\\v\\e\\n\\e\\c',
'Jun' => '\\Č\\e\\r\\v\\e\\n',
'Mar' => '\\B\\ř\\e\\z\\e\\n',
'May' => '\\K\\v\\ě\\t\\e\\n',
'Nov' => '\\L\\i\\s\\t\\o\\p\\a\\d',
'Oct' => '\\Ř\\í\\j\\e\\n',
'Sep' => '\\Z\\á\\ř\\í',
'apr' => 'dub',
'april' => 'Dub',
'aug' => 'srp',
'august' => 'Srp',
'before_yesterday' => 'Předevčírem',
'dec' => 'pro',
'december' => 'Pro',
'feb' => 'úno',
'february' => 'Úno',
'format_date' => 'j\\. %s Y',
'format_date_hour' => 'j\\. %s Y \\v H\\:i',
'fri' => 'Pá',
'jan' => 'led',
'january' => 'Led',
'jul' => 'čvn',
'july' => 'Čvn',
'jun' => 'čer',
'june' => 'Čer',
'last_3_month' => 'Minulé tři měsíce',
'last_6_month' => 'Minulých šest měsíců',
'last_month' => 'Minulý měsíc',
'last_week' => 'Minulý týden',
'last_year' => 'Minulý rok',
'mar' => 'bře',
'march' => 'Bře',
'may' => 'Kvě',
'mon' => 'Po',
'month' => 'měsíce',
'nov' => 'lis',
'november' => 'Lis',
'oct' => 'říj',
'october' => 'Říj',
'sat' => 'So',
'sep' => 'zář',
'september' => 'Zář',
'sun' => 'Ne',
'thu' => 'Čt',
'today' => 'Dnes',
'tue' => 'Út',
'wed' => 'St',
'yesterday' => 'Včera',
),
'freshrss' => array(
'_' => 'FreshRSS',
'about' => 'O FreshRSS',
),
'js' => array(
'category_empty' => 'Prázdná kategorie',
'confirm_action' => 'Jste si jist, že chcete provést tuto akci? Změny nelze vrátit zpět!',
'confirm_action_feed_cat' => 'Jste si jist, že chcete provést tuto akci? Přijdete o související oblíbené položky a uživatelské dotazy. Změny nelze vrátit zpět!',
'feedback' => array(
'body_new_articles' => 'Je %%d nových článků k přečtení v FreshRSS.',
'request_failed' => 'Požadavek selhal, což může být způsobeno problémy s připojení k internetu.',
'title_new_articles' => 'FreshRSS: nové články!',
),
'new_article' => 'Jsou k dispozici nové články, stránku obnovíte kliknutím zde.',
'should_be_activated' => 'JavaScript musí být povolen',
),
'lang' => array(
'cz' => 'Čeština',
'de' => 'Deutsch',
'en' => 'English',
'fr' => 'Français',
'it' => 'Italiano',
'nl' => 'Nederlands',
'ru' => 'Русский',
'tr' => 'Türkçe',
),
'menu' => array(
'about' => 'O aplikaci',
'admin' => 'Administrace',
'archiving' => 'Archivace',
'authentication' => 'Přihlášení',
'check_install' => 'Ověření instalace',
'configuration' => 'Nastavení',
'display' => 'Zobrazení',
'extensions' => 'Rozšíření',
'logs' => 'Logy',
'queries' => 'Uživatelské dotazy',
'reading' => 'Čtení',
'search' => 'Hledat výraz nebo #tagy',
'sharing' => 'Sdílení',
'shortcuts' => 'Zkratky',
'stats' => 'Statistika',
'system' => 'System configuration',// @todo translate
'update' => 'Aktualizace',
'user_management' => 'Správa uživatelů',
'user_profile' => 'Profil',
),
'pagination' => array(
'first' => 'První',
'last' => 'Poslední',
'load_more' => 'Načíst více článků',
'mark_all_read' => 'Označit vše jako přečtené',
'next' => 'Další',
'nothing_to_load' => 'Žádné nové články',
'previous' => 'Předchozí',
),
'share' => array(
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'movim' => 'Movim',
'print' => 'Tisk',
'shaarli' => 'Shaarli',
'twitter' => 'Twitter',
'wallabag' => 'wallabag v1',
'wallabagv2' => 'wallabag v2',
'jdh' => 'Journal du hacker',
),
'short' => array(
'attention' => 'Upozornění!',
'blank_to_disable' => 'Zakázat - ponechte prázdné',
'by_author' => 'Od <em>%s</em>',
'by_default' => 'Výchozí',
'damn' => 'Sakra!',
'default_category' => 'Nezařazeno',
'no' => 'Ne',
'ok' => 'Ok!',
'or' => 'nebo',
'yes' => 'Ano',
),
);

61
app/i18n/cz/index.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
return array(
'about' => array(
'_' => 'O FreshRSS',
'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
'bugs_reports' => 'Hlášení chyb',
'credits' => 'Poděkování',
'credits_content' => 'Některé designové prvky pocházejí z <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, FreshRSS ale tuto platformu nevyužívá. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ikony</a> pocházejí z <a href="https://www.gnome.org/">GNOME projektu</a>. Font <em>Open Sans</em> vytvořil <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS je založen na PHP framework <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.',
'freshrss_description' => 'FreshRSS je čtečka RSS kanálů určená k provozu na vlastním serveru, podobná <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> nebo <a href="http://projet.idleman.fr/leed/">Leed</a>. Je to nenáročný a jednoduchý, zároveň ale mocný a konfigurovatelný nástroj.',
'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">na Github</a>',
'license' => 'Licence',
'project_website' => 'Stránka projektu',
'title' => 'O FreshRSS',
'version' => 'Verze',
'website' => 'Webové stránka',
),
'feed' => array(
'add' => 'Můžete přidat kanály.',
'empty' => 'Žádné články k zobrazení.',
'rss_of' => 'RSS kanál %s',
'title' => 'RSS kanály',
'title_global' => 'Přehled',
'title_fav' => 'Oblíbené',
),
'log' => array(
'_' => 'Logy',
'clear' => 'Vymazat logy',
'empty' => 'Log je prázdný',
'title' => 'Logy',
),
'menu' => array(
'about' => 'O FreshRSS',
'add_query' => 'Vytvořit dotaz',
'before_one_day' => 'Den nazpět',
'before_one_week' => 'Před týdnem',
'favorites' => 'Oblíbené (%s)',
'global_view' => 'Přehled',
'main_stream' => 'Všechny kanály',
'mark_all_read' => 'Označit vše jako přečtené',
'mark_cat_read' => 'Označit kategorii jako přečtenou',
'mark_feed_read' => 'Označit kanál jako přečtený',
'newer_first' => 'Nové nejdříve',
'non-starred' => 'Zobrazit vše vyjma oblíbených',
'normal_view' => 'Normální',
'older_first' => 'Nejstarší nejdříve',
'queries' => 'Uživatelské dotazy',
'read' => 'Zobrazovat přečtené',
'reader_view' => 'Čtení',
'rss_view' => 'RSS kanál',
'search_short' => 'Hledat',
'starred' => 'Zobrazit oblíbené',
'stats' => 'Statistika',
'subscription' => 'Správa subskripcí',
'unread' => 'Zobrazovat nepřečtené',
),
'share' => 'Sdílet',
'tag' => array(
'related' => 'Související tagy',
),
);

119
app/i18n/cz/install.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
return array(
'action' => array(
'finish' => 'Dokončit instalaci',
'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.',
'keep_install' => 'Zachovat předchozí instalaci',
'next_step' => 'Přejít na další krok',
'reinstall' => 'Reinstalovat FreshRSS',
),
'auth' => array(
'form' => 'Webový formulář (tradiční, vyžaduje JavaScript)',
'http' => 'HTTP (pro pokročilé uživatele s HTTPS)',
'none' => 'Žádný (nebezpečné)',
'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
'password_format' => 'Alespoň 7 znaků',
'type' => 'Způsob přihlášení',
),
'bdd' => array(
'_' => 'Databáze',
'conf' => array(
'_' => 'Nastavení databáze',
'ko' => 'Ověřte informace o databázi.',
'ok' => 'Nastavení databáze bylo uloženo.',
),
'host' => 'Hostitel',
'prefix' => 'Prefix tabulky',
'password' => 'Heslo',
'type' => 'Typ databáze',
'username' => 'Uživatel',
),
'check' => array(
'_' => 'Kontrola',
'already_installed' => 'Zjistili jsme, že FreshRSS je již nainstalován!',
'cache' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/cache</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře cache jsou v pořádku.',
),
'ctype' => array(
'nok' => 'Není nainstalována požadovaná knihovna pro ověřování znaků (php-ctype).',
'ok' => 'Je nainstalována požadovaná knihovna pro ověřování znaků (ctype).',
),
'curl' => array(
'nok' => 'Nemáte cURL (balíček php-curl).',
'ok' => 'Máte rozšíření cURL.',
),
'data' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře data jsou v pořádku.',
),
'dom' => array(
'nok' => 'Nemáte požadovanou knihovnu pro procházení DOM.',
'ok' => 'Máte požadovanou knihovnu pro procházení DOM.',
),
'favicons' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/favicons</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře favicons jsou v pořádku.',
),
'fileinfo' => array(
'nok' => 'Nemáte PHP fileinfo (balíček fileinfo).',
'ok' => 'Máte rozšíření fileinfo.',
),
'http_referer' => array(
'nok' => 'Zkontrolujte prosím že neměníte HTTP REFERER.',
'ok' => 'Váš HTTP REFERER je znám a odpovídá Vašemu serveru.',
),
'json' => array(
'nok' => 'Pro parsování JSON chybí doporučená knihovna.',
'ok' => 'Máte doporučenou knihovnu pro parsování JSON.',
),
'minz' => array(
'nok' => 'Nemáte framework Minz.',
'ok' => 'Máte framework Minz.',
),
'pcre' => array(
'nok' => 'Nemáte požadovanou knihovnu pro regulární výrazy (php-pcre).',
'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
),
'pdo' => array(
'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.',
'ok' => 'Vaše verze PHP je %s a je kompatibilní s FreshRSS.',
),
'users' => array(
'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/users</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'ok' => 'Oprávnění adresáře users jsou v pořádku.',
),
'xml' => array(
'nok' => 'Pro parsování XML chybí požadovaná knihovna.',
'ok' => 'Máte požadovanou knihovnu pro parsování XML.',
),
),
'conf' => array(
'_' => 'Obecná nastavení',
'ok' => 'Nastavení bylo uloženo.',
),
'congratulations' => 'Gratulujeme!',
'default_user' => 'Jméno výchozího uživatele <small>(maximálně 16 alfanumerických znaků)</small>',
'delete_articles_after' => 'Smazat články starší než',
'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.',
'javascript_is_better' => 'Práce s FreshRSS je příjemnější se zapnutým JavaScriptem',
'js' => array(
'confirm_reinstall' => 'Reinstalací FreshRSS ztratíte předchozí konfiguraci. Opravdu chcete pokračovat?',
),
'language' => array(
'_' => 'Jazyk',
'choose' => 'Vyberte jazyk FreshRSS',
'defined' => 'Jazyk byl nastaven.',
),
'not_deleted' => 'Nastala chyba, soubor <em>%s</em> musíte smazat ručně.',
'ok' => 'Instalace byla úspěšná.',
'step' => 'krok %d',
'steps' => 'Kroky',
'title' => 'Instalace · FreshRSS',
'this_is_the_end' => 'Konec',
);

62
app/i18n/cz/sub.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
return array(
'category' => array(
'_' => 'Kategorie',
'add' => 'Přidat kategorii',
'empty' => 'Vyprázdit kategorii',
'new' => 'Nová kategorie',
),
'feed' => array(
'add' => 'Přidat RSS kanál',
'advanced' => 'Pokročilé',
'archiving' => 'Archivace',
'auth' => array(
'configuration' => 'Přihlášení',
'help' => 'Umožní přístup k RSS kanálům chráneným HTTP autentizací',
'http' => 'HTTP přihlášení',
'password' => 'Heslo',
'username' => 'Přihlašovací jméno',
),
'css_help' => 'Stáhne zkrácenou verzi RSS kanálů (pozor, náročnější na čas!)',
'css_path' => 'Původní CSS soubor článku z webových stránek',
'description' => 'Popis',
'empty' => 'Kanál je prázdný. Ověřte prosím zda je ještě autorem udržován.',
'error' => 'Vyskytl se problém s kanálem. Ověřte že je vždy dostupný, prosím, a poté jej aktualizujte.',
'in_main_stream' => 'Zobrazit ve “Všechny kanály”',
'informations' => 'Informace',
'keep_history' => 'Zachovat tento minimální počet článků',
'moved_category_deleted' => 'Po smazání kategorie budou v ní obsažené kanály automaticky přesunuty do <em>%s</em>.',
'no_selected' => 'Nejsou označeny žádné kanály.',
'number_entries' => '%d článků',
'stats' => 'Statistika',
'think_to_add' => 'Můžete přidat kanály.',
'title' => 'Název',
'title_add' => 'Přidat RSS kanál',
'ttl' => 'Neobnovovat častěji než',
'url' => 'URL kanálu',
'validator' => 'Zkontrolovat platnost kanálu',
'website' => 'URL webové stránky',
'pubsubhubbub' => 'Okamžité oznámení s PubSubHubbub',
),
'import_export' => array(
'export' => 'Export',
'export_opml' => 'Exportovat seznam kanálů (OPML)',
'export_starred' => 'Exportovat oblíbené',
'feed_list' => 'Seznam %s článků',
'file_to_import' => 'Soubor k importu<br />(OPML, JSON nebo ZIP)',
'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo JSON)',
'import' => 'Import',
'starred_list' => 'Seznam oblíbených článků',
'title' => 'Import / export',
),
'menu' => array(
'bookmark' => 'Přihlásit (FreshRSS bookmark)',
'import_export' => 'Import / export',
'subscription_management' => 'Správa subskripcí',
),
'title' => array(
'_' => 'Správa subskripcí',
'feed_management' => 'Správa RSS kanálů',
),
);

181
app/i18n/de/admin.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
return array(
'auth' => array(
'allow_anonymous' => 'Anonymes Lesen der Artikel des Standardbenutzers (%s) erlauben',
'allow_anonymous_refresh' => 'Anonymes Aktualisieren der Artikel erlauben',
'api_enabled' => '<abbr>API</abbr>-Zugriff erlauben <small>(für mobile Anwendungen benötigt)</small>',
'form' => 'Webformular (traditionell, benötigt JavaScript)',
'http' => 'HTTP (HTTPS für erfahrene Benutzer)',
'none' => 'Keine (gefährlich)',
'title' => 'Authentifizierung',
'title_reset' => 'Zurücksetzen der Authentifizierung',
'token' => 'Authentifizierungs-Token',
'token_help' => 'Erlaubt den Zugriff auf die RSS-Ausgabe des Standardbenutzers ohne Authentifizierung.',
'type' => 'Authentifizierungsmethode',
'unsafe_autologin' => 'Erlaube unsicheres automatisches Anmelden mit folgendem Format: ',
),
'check_install' => array(
'cache' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/cache</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/cache</em> sind in Ordnung.',
),
'categories' => array(
'nok' => 'Die Tabelle <em>category</em> ist schlecht konfiguriert.',
'ok' => 'Die Tabelle <em>category</em> ist korrekt konfiguriert.',
),
'connection' => array(
'nok' => 'Verbindung zur Datenbank kann nicht aufgebaut werden.',
'ok' => 'Verbindung zur Datenbank konnte aufgebaut werden.',
),
'ctype' => array(
'nok' => 'Ihnen fehlt eine benötigte Bibliothek für die Überprüfung von Zeichentypen (php-ctype).',
'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
),
'curl' => array(
'nok' => 'Ihnen fehlt cURL (Paket php-curl).',
'ok' => 'Sie haben die cURL-Erweiterung.',
),
'data' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data</em> sind in Ordnung.',
),
'database' => 'Datenbank-Installation',
'dom' => array(
'nok' => 'Ihnen fehlt eine benötigte Bibliothek um DOM zu durchstöbern (Paket php-xml).',
'ok' => 'Sie haben die benötigte Bibliothek um DOM zu durchstöbern.',
),
'entries' => array(
'nok' => 'Die Tabelle <em>entry</em> ist schlecht konfiguriert.',
'ok' => 'Die Tabelle <em>entry</em> ist korrekt konfiguriert.',
),
'favicons' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/favicons</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/favicons</em> sind in Ordnung.',
),
'feeds' => array(
'nok' => 'Die Tabelle <em>feed</em> ist schlecht konfiguriert.',
'ok' => 'Die Tabelle <em>feed</em> ist korrekt konfiguriert.',
),
'fileinfo' => array(
'nok' => 'Ihnen fehlt PHP fileinfo (Paket fileinfo).',
'ok' => 'Sie haben die fileinfo-Erweiterung.',
),
'files' => 'Datei-Installation',
'json' => array(
'nok' => 'Ihnen fehlt die JSON-Erweiterung (Paket php5-json).',
'ok' => 'Sie haben die JSON-Erweiterung.',
),
'minz' => array(
'nok' => 'Ihnen fehlt das Minz-Framework.',
'ok' => 'Sie haben das Minz-Framework.',
),
'pcre' => array(
'nok' => 'Ihnen fehlt eine benötigte Bibliothek für reguläre Ausdrücke (php-pcre).',
'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
),
'pdo' => array(
'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP-Installation',
'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.',
'ok' => 'Ihre PHP-Version ist %s, welche kompatibel mit FreshRSS ist.',
),
'tables' => array(
'nok' => 'Es fehlen eine oder mehrere Tabellen in der Datenbank.',
'ok' => 'Tabellen existieren in der Datenbank.',
),
'title' => 'Installationsüberprüfung',
'tokens' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/tokens</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/tokens</em> sind in Ordnung.',
),
'users' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/users</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/users</em> sind in Ordnung.',
),
'zip' => array(
'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php-zip).',
'ok' => 'Sie haben die ZIP-Erweiterung.',
),
),
'extensions' => array(
'disabled' => 'Deaktiviert',
'empty_list' => 'Es gibt keine installierte Erweiterung.',
'enabled' => 'Aktiviert',
'no_configure_view' => 'Diese Erweiterung kann nicht konfiguriert werden.',
'system' => array(
'_' => 'System-Erweiterungen',
'no_rights' => 'System-Erweiterung (Sie haben keine Berechtigung dafür)',
),
'title' => 'Erweiterungen',
'user' => 'Benutzer-Erweiterungen',
),
'stats' => array(
'_' => 'Statistiken',
'all_feeds' => 'Alle Feeds',
'category' => 'Kategorie',
'entry_count' => 'Anzahl der Einträge',
'entry_per_category' => 'Einträge pro Kategorie',
'entry_per_day' => 'Einträge pro Tag (letzten 30 Tage)',
'entry_per_day_of_week' => 'Pro Wochentag (Durchschnitt: %.2f Nachrichten)',
'entry_per_hour' => 'Pro Stunde (Durchschnitt: %.2f Nachrichten)',
'entry_per_month' => 'Pro Monat (Durchschnitt: %.2f Nachrichten)',
'entry_repartition' => 'Einträge-Verteilung',
'feed' => 'Feed',
'feed_per_category' => 'Feeds pro Kategorie',
'idle' => 'Inaktive Feeds',
'main' => 'Haupt-Statistiken',
'main_stream' => 'Haupt-Feeds',
'menu' => array(
'idle' => 'Inaktive Feeds',
'main' => 'Haupt-Statistiken',
'repartition' => 'Artikel-Verteilung',
),
'no_idle' => 'Es gibt keinen inaktiven Feed!',
'number_entries' => '%d Artikel',
'percent_of_total' => '%% Gesamt',
'repartition' => 'Artikel-Verteilung',
'status_favorites' => 'Favoriten',
'status_read' => 'Gelesen',
'status_total' => 'Gesamt',
'status_unread' => 'Ungelesen',
'title' => 'Statistiken',
'top_feed' => 'Top 10-Feeds',
),
'system' => array(
'_' => 'System configuration', // @todo translate
'auto-update-url' => 'Auto-update server URL', // @todo translate
'instance-name' => 'Instance name', // @todo translate
'max-categories' => 'Categories per user limit', // @todo translate
'max-feeds' => 'Feeds per user limit', // @todo translate
'registration' => array(
'help' => '0 meint, dass es kein Account Limit gibt',
'number' => 'Maximale Anzahl von Accounts',
),
),
'update' => array(
'_' => 'System aktualisieren',
'apply' => 'Anwenden',
'check' => 'Auf neue Aktualisierungen prüfen',
'current_version' => 'Ihre aktuelle Version von FreshRSS ist %s.',
'last' => 'Letzte Überprüfung: %s',
'none' => 'Keine ausstehende Aktualisierung',
'title' => 'System aktualisieren',
),
'user' => array(
'articles_and_size' => '%s Artikel (%s)',
'create' => 'Neuen Benutzer erstellen',
'language' => 'Sprache',
'number' => 'Es wurde bis jetzt %d Account erstellt',
'numbers' => 'Es wurden bis jetzt %d Accounts erstellt',
'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
'password_format' => 'mindestens 7 Zeichen',
'title' => 'Benutzer verwalten',
'user_list' => 'Liste der Benutzer',
'username' => 'Nutzername',
'users' => 'Benutzer',
),
);

173
app/i18n/de/conf.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
return array(
'archiving' => array(
'_' => 'Archivierung',
'advanced' => 'Erweitert',
'delete_after' => 'Entferne Artikel nach',
'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Feeds verfügbar.',
'keep_history_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden',
'optimize' => 'Datenbank optimieren',
'optimize_help' => 'Sollte gelegentlich durchgeführt werden, um die Größe der Datenbank zu reduzieren.',
'purge_now' => 'Jetzt bereinigen',
'title' => 'Archivierung',
'ttl' => 'Aktualisiere automatisch nicht öfter als',
),
'display' => array(
'_' => 'Anzeige',
'icon' => array(
'bottom_line' => 'Fußzeile',
'entry' => 'Artikel-Symbole',
'publication_date' => 'Datum der Veröffentlichung',
'related_tags' => 'Verwandte Tags',
'sharing' => 'Teilen',
'top_line' => 'Kopfzeile',
),
'language' => 'Sprache',
'notif_html5' => array(
'seconds' => 'Sekunden (0 bedeutet keine Zeitüberschreitung)',
'timeout' => 'Zeitüberschreitung für HTML5-Benachrichtigung',
),
'theme' => 'Erscheinungsbild',
'title' => 'Anzeige',
'width' => array(
'content' => 'Inhaltsbreite',
'large' => 'Gross',
'medium' => 'Mittel',
'no_limit' => 'Keine Begrenzung',
'thin' => 'Klein',
),
),
'query' => array(
'_' => 'Benutzerabfragen',
'deprecated' => 'Diese Abfrage ist nicht länger gültig. Die referenzierte Kategorie oder der Feed ist gelöscht worden.',
'filter' => 'Angewendeter Filter:',
'get_all' => 'Alle Artikel anzeigen',
'get_category' => 'Kategorie "%s" anzeigen',
'get_favorite' => 'Lieblingsartikel anzeigen',
'get_feed' => 'Feed "%s" anzeigen',
'no_filter' => 'Kein Filter',
'none' => 'Sie haben bisher keine Benutzerabfrage erstellt.',
'number' => 'Abfrage Nr. %d',
'order_asc' => 'Älteste Artikel zuerst anzeigen',
'order_desc' => 'Neueste Artikel zuerst anzeigen',
'search' => 'Suche nach "%s"',
'state_0' => 'Alle Artikel anzeigen',
'state_1' => 'Gelesene Artikel anzeigen',
'state_2' => 'Ungelesene Artikel anzeigen',
'state_3' => 'Alle Artikel anzeigen',
'state_4' => 'Lieblingsartikel anzeigen',
'state_5' => 'Gelesene Lieblingsartikel anzeigen',
'state_6' => 'Ungelesene Lieblingsartikel anzeigen',
'state_7' => 'Lieblingsartikel anzeigen',
'state_8' => 'Keine Lieblingsartikel anzeigen',
'state_9' => 'Gelesene ohne Lieblingsartikel anzeigen',
'state_10' => 'Ungelesene ohne Lieblingsartikel anzeigen',
'state_11' => 'Keine Lieblingsartikel anzeigen',
'state_12' => 'Alle Artikel anzeigen',
'state_13' => 'Gelesene Artikel anzeigen',
'state_14' => 'Ungelesene Artikel anzeigen',
'state_15' => 'Alle Artikel anzeigen',
'title' => 'Benutzerabfragen',
),
'profile' => array(
'_' => 'Profil-Verwaltung',
'delete' => array(
'_' => 'Accountlöschung',
'warn' => 'Dein Account und alle damit bezogenen Daten werden gelöscht.',
),
'password_api' => 'Passwort-API<br /><small>(z. B. für mobile Anwendungen)</small>',
'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
'password_format' => 'mindestens 7 Zeichen',
'title' => 'Profil',
),
'reading' => array(
'_' => 'Lesen',
'after_onread' => 'Nach „Alle als gelesen markieren“,',
'articles_per_page' => 'Anzahl der Artikel pro Seite',
'auto_load_more' => 'Die nächsten Artikel am Seitenende laden',
'auto_remove_article' => 'Artikel nach dem Lesen verstecken',
'mark_updated_article_unread' => 'Markieren Sie aktualisierte Artikel als ungelesen',
'confirm_enabled' => 'Bei der Aktion „Alle als gelesen markieren“ einen Bestätigungsdialog anzeigen',
'display_articles_unfolded' => 'Artikel standardmäßig ausgeklappt zeigen',
'display_categories_unfolded' => 'Kategorien standardmäßig eingeklappt zeigen',
'hide_read_feeds' => 'Kategorien & Feeds ohne ungelesene Artikel verstecken (funktioniert nicht mit der Einstellung „Alle Artikel zeigen“)',
'img_with_lazyload' => 'Verwende die "träges Laden"-Methode zum Laden von Bildern',
'jump_next' => 'springe zum nächsten ungelesenen Geschwisterelement (Feed oder Kategorie)',
'number_divided_when_reader' => 'Geteilt durch 2 in der Lese-Ansicht.',
'read' => array(
'article_open_on_website' => 'wenn der Artikel auf der Original-Webseite geöffnet wird',
'article_viewed' => 'wenn der Artikel angesehen wird',
'scroll' => 'beim Blättern',
'upon_reception' => 'beim Empfang des Artikels',
'when' => 'Artikel als gelesen markieren…',
),
'show' => array(
'_' => 'Artikel zum Anzeigen',
'adaptive' => 'Anzeige anpassen',
'all_articles' => 'Alle Artikel zeigen',
'unread' => 'Nur ungelesene zeigen',
),
'sort' => array(
'_' => 'Sortierreihenfolge',
'newer_first' => 'Neuere zuerst',
'older_first' => 'Ältere zuerst',
),
'sticky_post' => 'Wenn geöffnet, den Artikel ganz oben anheften',
'title' => 'Lesen',
'view' => array(
'default' => 'Standard-Ansicht',
'global' => 'Globale Ansicht',
'normal' => 'Normale Ansicht',
'reader' => 'Lese-Ansicht',
),
),
'sharing' => array(
'_' => 'Teilen',
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'E-Mail',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Weitere Informationen',
'print' => 'Drucken',
'shaarli' => 'Shaarli',
'share_name' => 'Anzuzeigender Teilen-Name',
'share_url' => 'Zu verwendende Teilen-URL',
'title' => 'Teilen',
'twitter' => 'Twitter',
'wallabag' => 'wallabag',
),
'shortcut' => array(
'_' => 'Tastenkombination',
'article_action' => 'Artikelaktionen',
'auto_share' => 'Teilen',
'auto_share_help' => 'Wenn es nur eine Option zum Teilen gibt, wird diese verwendet. Ansonsten sind die Optionen über ihre Nummer erreichbar.',
'close_dropdown' => 'Menüs schließen',
'collapse_article' => 'Einklappen',
'first_article' => 'Zum ersten Artikel springen',
'focus_search' => 'Auf das Suchfeld zugreifen',
'help' => 'Dokumentation anzeigen',
'javascript' => 'JavaScript muss aktiviert sein, um Tastaturkürzel benutzen zu können',
'last_article' => 'Zum letzten Artikel springen',
'load_more' => 'Weitere Artikel laden',
'mark_read' => 'Als gelesen markieren',
'mark_favorite' => 'Als Favorit markieren',
'navigation' => 'Navigation',
'navigation_help' => 'Mit der "Umschalttaste" finden die Tastenkombination auf Feeds Anwendung.<br/>Mit der "Alt-Taste" finden die Tastenkombination auf Kategorien Anwendung.',
'next_article' => 'Zum nächsten Artikel springen',
'other_action' => 'Andere Aktionen',
'previous_article' => 'Zum vorherigen Artikel springen',
'see_on_website' => 'Auf der Original-Webseite ansehen',
'shift_for_all_read' => '+ <code>Umschalttaste</code>, um alle Artikel als gelesen zu markieren.',
'title' => 'Tastenkombination',
'user_filter' => 'Auf Benutzerfilter zugreifen',
'user_filter_help' => 'Wenn es nur einen Benutzerfilter gibt, wird dieser verwendet. Ansonsten sind die Filter über ihre Nummer erreichbar.',
),
'user' => array(
'articles_and_size' => '%s Artikel (%s)',
'current' => 'Aktueller Benutzer',
'is_admin' => 'ist Administrator',
'users' => 'Benutzer',
),
);

109
app/i18n/de/feedback.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
return array(
'admin' => array(
'optimization_complete' => 'Optimierung abgeschlossen',
),
'access' => array(
'denied' => 'Sie haben nicht die Berechtigung, diese Seite aufzurufen',
'not_found' => 'Sie suchen nach einer Seite, die nicht existiert',
),
'auth' => array(
'form' => array(
'not_set' => 'Während der Konfiguration des Authentifikationssystems trat ein Fehler auf. Bitte versuchen Sie es später erneut.',
'set' => 'Formular ist ab sofort ihr Standard-Authentifikationssystem.',
),
'login' => array(
'invalid' => 'Anmeldung ist ungültig',
'success' => 'Sie sind angemeldet',
),
'logout' => array(
'success' => 'Sie sind abgemeldet',
),
'no_password_set' => 'Administrator-Passwort ist nicht gesetzt worden. Dieses Feature ist nicht verfügbar.',
),
'conf' => array(
'error' => 'Während der Speicherung der Konfiguration trat ein Fehler auf',
'query_created' => 'Abfrage "%s" ist erstellt worden.',
'shortcuts_updated' => 'Die Tastenkombinationen sind aktualisiert worden',
'updated' => 'Die Konfiguration ist aktualisiert worden',
),
'extensions' => array(
'already_enabled' => '%s ist bereits aktiviert',
'disable' => array(
'ko' => '%s kann nicht deaktiviert werden. Für Details <a href="%s">prüfen Sie die FressRSS-Protokolle</a>.',
'ok' => '%s ist jetzt deaktiviert',
),
'enable' => array(
'ko' => '%s kann nicht aktiviert werden. Für Details <a href="%s">prüfen Sie die FressRSS-Protokolle</a>.',
'ok' => '%s ist jetzt aktiviert',
),
'no_access' => 'Sie haben keinen Zugang zu %s',
'not_enabled' => '%s ist noch nicht aktiviert',
'not_found' => '%s existiert nicht',
),
'import_export' => array(
'export_no_zip_extension' => 'Die ZIP-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie die Dateien eine nach der anderen zu exportieren.',
'feeds_imported' => 'Ihre Feeds sind importiert worden und werden jetzt aktualisiert',
'feeds_imported_with_errors' => 'Ihre Feeds sind importiert worden, aber es traten einige Fehler auf',
'file_cannot_be_uploaded' => 'Die Datei kann nicht hochgeladen werden!',
'no_zip_extension' => 'Die ZIP-Erweiterung ist auf Ihrem Server nicht vorhanden.',
'zip_error' => 'Ein Fehler trat während des ZIP-Imports auf.',
),
'sub' => array(
'actualize' => 'Aktualisieren',
'category' => array(
'created' => 'Die Kategorie %s ist erstellt worden.',
'deleted' => 'Die Kategorie ist gelöscht worden.',
'emptied' => 'Die Kategorie ist geleert worden.',
'error' => 'Die Kategorie kann nicht aktualisiert werden',
'name_exists' => 'Der Kategorie-Name existiert bereits.',
'no_id' => 'Sie müssen die ID der Kategorie präzisieren.',
'no_name' => 'Der Kategorie-Name kann nicht leer sein.',
'not_delete_default' => 'Sie können die Vorgabe-Kategorie nicht löschen!',
'not_exist' => 'Die Kategorie existiert nicht!',
'over_max' => 'Sie haben Ihre Kategorien-Limite erreicht (%d)',
'updated' => 'Die Kategorie ist aktualisiert worden.',
),
'feed' => array(
'actualized' => '<em>%s</em> ist aktualisiert worden',
'actualizeds' => 'Die RSS-Feeds sind aktualisiert worden',
'added' => 'Der RSS-Feed <em>%s</em> ist hinzugefügt worden',
'already_subscribed' => 'Sie haben <em>%s</em> bereits abonniert',
'deleted' => 'Der Feed ist gelöscht worden',
'error' => 'Der Feed kann nicht aktualisiert werden',
'internal_problem' => 'Der RSS-Feed konnte nicht hinzugefügt werden. Für Details <a href="%s">prüfen Sie die FressRSS-Protokolle</a>.',
'invalid_url' => 'Die URL <em>%s</em> ist ungültig',
'marked_read' => 'Die Feeds sind als gelesen markiert worden',
'n_actualized' => 'Die %d Feeds sind aktualisiert worden',
'n_entries_deleted' => 'Die %d Artikel sind gelöscht worden',
'no_refresh' => 'Es gibt keinen Feed zum Aktualisieren…',
'not_added' => '<em>%s</em> konnte nicht hinzugefügt werden',
'over_max' => 'Sie haben Ihre Feeds-Limite erreicht (%d)',
'updated' => 'Der Feed ist aktualisiert worden',
),
'purge_completed' => 'Bereinigung abgeschlossen (%d Artikel gelöscht)',
),
'update' => array(
'can_apply' => 'FreshRSS wird nun auf die <strong>Version %s</strong> aktualisiert.',
'error' => 'Der Aktualisierungsvorgang stieß auf einen Fehler: %s',
'file_is_nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>%s</em>. Der HTTP-Server muss Schreibrechte besitzen',
'finished' => 'Aktualisierung abgeschlossen!',
'none' => 'Keine Aktualisierung zum Anwenden',
'server_not_found' => 'Der Aktualisierungs-Server kann nicht gefunden werden. [%s]',
),
'user' => array(
'created' => array(
'_' => 'Der Benutzer %s ist erstellt worden',
'error' => 'Der Benutzer %s kann nicht erstellt werden',
),
'deleted' => array(
'_' => 'Der Benutzer %s ist gelöscht worden',
'error' => 'Der Benutzer %s kann nicht gelöscht werden',
),
),
'profile' => array(
'error' => 'Ihr Profil kann nicht geändert werden',
'updated' => 'Ihr Profil ist geändert worden',
),
);

182
app/i18n/de/gen.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
return array(
'action' => array(
'actualize' => 'Aktualisieren',
'back_to_rss_feeds' => '← Zurück zu Ihren RSS-Feeds gehen',
'cancel' => 'Abbrechen',
'create' => 'Erstellen',
'disable' => 'Deaktivieren',
'empty' => 'Leeren',
'enable' => 'Aktivieren',
'export' => 'Exportieren',
'filter' => 'Filtern',
'import' => 'Importieren',
'manage' => 'Verwalten',
'mark_read' => 'Als gelesen markieren',
'mark_favorite' => 'Als Favorit markieren',
'remove' => 'Entfernen',
'see_website' => 'Webseite ansehen',
'submit' => 'Abschicken',
'truncate' => 'Alle Artikel löschen',
),
'auth' => array(
'email' => 'E-Mail-Adresse',
'keep_logged_in' => 'Eingeloggt bleiben <small>(%s Tage)</small>',
'login' => 'Anmelden',
'logout' => 'Abmelden',
'password' => array(
'_' => 'Passwort',
'format' => '<small>mindestens 7 Zeichen</small>',
),
'registration' => array(
'_' => 'Neuer Account',
'ask' => 'Erstelle einen Account?',
'title' => 'Accounterstellung',
),
'reset' => 'Zurücksetzen der Authentifizierung',
'username' => array(
'_' => 'Nutzername',
'admin' => 'Administrator-Nutzername',
'format' => '<small>maximal 16 alphanumerische Zeichen</small>',
),
),
'date' => array(
'Apr' => '\\A\\p\\r\\i\\l',
'Aug' => '\\A\\u\\g\\u\\s\\t',
'Dec' => '\\D\\e\\z\\e\\m\\b\\e\\r',
'Feb' => '\\F\\e\\b\\r\\u\\a\\r',
'Jan' => '\\J\\a\\n\\u\\a\\r',
'Jul' => '\\J\\u\\l\\i',
'Jun' => '\\J\\u\\n\\i',
'Mar' => '\\M\\ä\\r\\z',
'May' => '\\M\\a\\i',
'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r',
'Oct' => '\\O\\k\\t\\o\\b\\e\\r',
'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r',
'apr' => 'Apr',
'april' => 'April',
'aug' => 'Aug',
'august' => 'August',
'before_yesterday' => 'Vor vorgestern',
'dec' => 'Dez',
'december' => 'Dezember',
'feb' => 'Feb',
'february' => 'Februar',
'format_date' => 'd\\. %s Y',
'format_date_hour' => 'd\\. %s Y \\u\\m H\\:i',
'fri' => 'Fr',
'jan' => 'Jan',
'january' => 'Januar',
'jul' => 'Jul',
'july' => 'Juli',
'jun' => 'Jun',
'june' => 'Juni',
'last_3_month' => 'Letzte drei Monate',
'last_6_month' => 'Letzte sechs Monate',
'last_month' => 'Letzter Monat',
'last_week' => 'Letzte Woche',
'last_year' => 'Letztes Jahr',
'mar' => 'Mär',
'march' => 'März',
'may' => 'Mai',
'mon' => 'Mo',
'month' => 'Monat(en)',
'nov' => 'Nov',
'november' => 'November',
'oct' => 'Okt',
'october' => 'Oktober',
'sat' => 'Sa',
'sep' => 'Sep',
'september' => 'September',
'sun' => 'So',
'thu' => 'Do',
'today' => 'Heute',
'tue' => 'Di',
'wed' => 'Mi',
'yesterday' => 'Gestern',
),
'freshrss' => array(
'_' => 'FreshRSS',
'about' => 'Über FreshRSS',
),
'js' => array(
'category_empty' => 'Kategorie leeren',
'confirm_action' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Diese Aktion kann nicht abgebrochen werden!',
'confirm_action_feed_cat' => 'Sind Sie sicher, dass Sie diese Aktion durchführen wollen? Sie werden zugehörige Favoriten und Benutzerabfragen verlieren. Dies kann nicht abgebrochen werden!',
'feedback' => array(
'body_new_articles' => 'Es gibt %%d neue Artikel zum Lesen auf FreshRSS.',
'request_failed' => 'Eine Anfrage ist fehlgeschlagen, dies könnte durch Probleme mit der Internetverbindung verursacht worden sein.',
'title_new_articles' => 'FreshRSS: neue Artikel!',
),
'new_article' => 'Es gibt neue verfügbare Artikel. Klicken Sie, um die Seite zu aktualisieren.',
'should_be_activated' => 'JavaScript muss aktiviert sein',
),
'lang' => array(
'cz' => 'Čeština',
'de' => 'Deutsch',
'en' => 'English',
'fr' => 'Français',
'it' => 'Italiano',
'nl' => 'Nederlands',
'ru' => 'Русский',
'tr' => 'Türkçe',
),
'menu' => array(
'about' => 'Über',
'admin' => 'Administration',
'archiving' => 'Archivierung',
'authentication' => 'Authentifizierung',
'check_install' => 'Installationsüberprüfung',
'configuration' => 'Konfiguration',
'display' => 'Anzeige',
'extensions' => 'Erweiterungen',
'logs' => 'Protokolle',
'queries' => 'Benutzerabfragen',
'reading' => 'Lesen',
'search' => 'Suche Worte oder #Tags',
'sharing' => 'Teilen',
'shortcuts' => 'Tastaturkürzel',
'stats' => 'Statistiken',
'system' => 'System configuration',// @todo translate
'update' => 'Aktualisieren',
'user_management' => 'Benutzer verwalten',
'user_profile' => 'Profil',
),
'pagination' => array(
'first' => 'Erste',
'last' => 'Letzte',
'load_more' => 'Weitere Artikel laden',
'mark_all_read' => 'Alle als gelesen markieren',
'next' => 'Nächste',
'nothing_to_load' => 'Es gibt keine weiteren Artikel',
'previous' => 'Vorherige',
),
'share' => array(
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'E-Mail',
'facebook' => 'Facebook',
'g+' => 'Google+',
'movim' => 'Movim',
'print' => 'Drucken',
'shaarli' => 'Shaarli',
'twitter' => 'Twitter',
'wallabag' => 'wallabag v1',
'wallabagv2' => 'wallabag v2',
'jdh' => 'Journal du hacker',
),
'short' => array(
'attention' => 'Achtung!',
'blank_to_disable' => 'Zum Deaktivieren frei lassen',
'by_author' => 'Von <em>%s</em>',
'by_default' => 'standardmäßig',
'damn' => 'Verdammt!',
'default_category' => 'Unkategorisiert',
'no' => 'Nein',
'not_applicable' => 'Nicht verfügbar',
'ok' => 'OK!',
'or' => 'oder',
'yes' => 'Ja',
),
);

61
app/i18n/de/index.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
return array(
'about' => array(
'_' => 'Über',
'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
'bugs_reports' => 'Fehlerberichte',
'credits' => 'Credits',
'credits_content' => 'Einige Designelemente stammen von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> stammen vom <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> Font wurde von <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> erstellt. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP-Framework.',
'freshrss_description' => 'FreshRSS ist ein RSS-Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="http://projet.idleman.fr/leed/">Leed</a>. Er ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstarkes und konfigurierbares Werkzeug.',
'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
'license' => 'Lizenz',
'project_website' => 'Projekt-Webseite',
'title' => 'Über',
'version' => 'Version',
'website' => 'Webseite',
),
'feed' => array(
'add' => 'Sie können Feeds hinzufügen.',
'empty' => 'Es gibt keinen Artikel zum Anzeigen.',
'rss_of' => 'RSS-Feed von %s',
'title' => 'Ihre RSS-Feeds',
'title_global' => 'Globale Ansicht',
'title_fav' => 'Ihre Favoriten',
),
'log' => array(
'_' => 'Protokolle',
'clear' => 'Protokolle leeren',
'empty' => 'Protokolldatei ist leer.',
'title' => 'Protokolle',
),
'menu' => array(
'about' => 'Über FreshRSS',
'add_query' => 'Eine Abfrage hinzufügen',
'before_one_day' => 'Vor einem Tag',
'before_one_week' => 'Vor einer Woche',
'favorites' => 'Favoriten (%s)',
'global_view' => 'Globale Ansicht',
'main_stream' => 'Haupt-Feeds',
'mark_all_read' => 'Alle als gelesen markieren',
'mark_cat_read' => 'Kategorie als gelesen markieren',
'mark_feed_read' => 'Feed als gelesen markieren',
'newer_first' => 'Neuere zuerst',
'non-starred' => 'Alle außer Favoriten zeigen',
'normal_view' => 'Normale Ansicht',
'older_first' => 'Ältere zuerst',
'queries' => 'Benutzerabfragen',
'read' => 'Nur gelesene zeigen',
'reader_view' => 'Lese-Ansicht',
'rss_view' => 'RSS-Feed',
'search_short' => 'Suchen',
'starred' => 'Nur Favoriten zeigen',
'stats' => 'Statistiken',
'subscription' => 'Abonnementverwaltung',
'unread' => 'Nur ungelesene zeigen',
),
'share' => 'Teilen',
'tag' => array(
'related' => 'Verwandte Tags',
),
);

119
app/i18n/de/install.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
return array(
'action' => array(
'finish' => 'Installation fertigstellen',
'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
'keep_install' => 'Vorherige Konfiguration beibehalten',
'next_step' => 'Zum nächsten Schritt springen',
'reinstall' => 'Neuinstallation von FreshRSS',
),
'auth' => array(
'form' => 'Webformular (traditionell, benötigt JavaScript)',
'http' => 'HTTP (HTTPS für erfahrene Benutzer)',
'none' => 'Keine (gefährlich)',
'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
'password_format' => 'mindestens 7 Zeichen',
'type' => 'Authentifizierungsmethode',
),
'bdd' => array(
'_' => 'Datenbank',
'conf' => array(
'_' => 'Datenbank-Konfiguration',
'ko' => 'Überprüfen Sie Ihre Datenbank-Information.',
'ok' => 'Datenbank-Konfiguration ist gespeichert worden.',
),
'host' => 'Host',
'prefix' => 'Tabellen-Präfix',
'password' => 'SQL-Password',
'type' => 'Datenbank-Typ',
'username' => 'SQL-Nutzername',
),
'check' => array(
'_' => 'Überprüfungen',
'already_installed' => 'Wir haben festgestellt, dass FreshRSS bereits installiert wurde!',
'cache' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/cache</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/cache</em> sind in Ordnung.',
),
'ctype' => array(
'nok' => 'Ihnen fehlt eine benötigte Bibliothek für die Überprüfung von Zeichentypen (php-ctype).',
'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).',
),
'curl' => array(
'nok' => 'Ihnen fehlt cURL (Paket php-curl).',
'ok' => 'Sie haben die cURL-Erweiterung.',
),
'data' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data</em> sind in Ordnung.',
),
'dom' => array(
'nok' => 'Ihnen fehlt eine benötigte Bibliothek um DOM zu durchstöbern.',
'ok' => 'Sie haben die benötigte Bibliothek um DOM zu durchstöbern.',
),
'favicons' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/favicons</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/favicons</em> sind in Ordnung.',
),
'fileinfo' => array(
'nok' => 'Ihnen fehlt PHP fileinfo (Paket fileinfo).',
'ok' => 'Sie haben die fileinfo-Erweiterung.',
),
'http_referer' => array(
'nok' => 'Bitte stellen Sie sicher, dass Sie Ihren HTTP REFERER nicht abändern.',
'ok' => 'Ihr HTTP REFERER ist bekannt und entspricht Ihrem Server.',
),
'json' => array(
'nok' => 'Ihnen fehlt eine empfohlene Bibliothek um JSON zu parsen.',
'ok' => 'Sie haben eine empfohlene Bibliothek um JSON zu parsen.',
),
'minz' => array(
'nok' => 'Ihnen fehlt das Minz-Framework.',
'ok' => 'Sie haben das Minz-Framework.',
),
'pcre' => array(
'nok' => 'Ihnen fehlt eine benötigte Bibliothek für reguläre Ausdrücke (php-pcre).',
'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
),
'pdo' => array(
'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.',
'ok' => 'Ihre PHP-Version ist %s, welche kompatibel mit FreshRSS ist.',
),
'users' => array(
'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/users</em>. Der HTTP-Server muss Schreibrechte besitzen.',
'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/users</em> sind in Ordnung.',
),
'xml' => array(
'nok' => 'Ihnen fehlt die benötigte Bibliothek um XML zu parsen.',
'ok' => 'Sie haben die benötigte Bibliothek um XML zu parsen.',
),
),
'conf' => array(
'_' => 'Allgemeine Konfiguration',
'ok' => 'Die allgemeine Konfiguration ist gespeichert worden.',
),
'congratulations' => 'Glückwunsch!',
'default_user' => 'Nutzername des Standardbenutzers <small>(maximal 16 alphanumerische Zeichen)</small>',
'delete_articles_after' => 'Entferne Artikel nach',
'fix_errors_before' => 'Bitte den Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
'javascript_is_better' => 'FreshRSS ist ansprechender mit aktiviertem JavaScript',
'js' => array(
'confirm_reinstall' => 'Du wirst deine vorherige Konfiguration (Daten) verlieren FreshRSS. Bist du sicher, dass du fortfahren willst?',
),
'language' => array(
'_' => 'Sprache',
'choose' => 'Wählen Sie eine Sprache für FreshRSS',
'defined' => 'Die Sprache ist festgelegt worden.',
),
'not_deleted' => 'Etwas ist schiefgelaufen; Sie müssen die Datei <em>%s</em> manuell löschen.',
'ok' => 'Der Installationsvorgang war erfolgreich.',
'step' => 'Schritt %d',
'steps' => 'Schritte',
'title' => 'Installation · FreshRSS',
'this_is_the_end' => 'Das ist das Ende',
);

62
app/i18n/de/sub.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
return array(
'category' => array(
'_' => 'Kategorie',
'add' => 'Eine Kategorie hinzufügen',
'empty' => 'Leere Kategorie',
'new' => 'Neue Kategorie',
),
'feed' => array(
'add' => 'Einen RSS-Feed hinzufügen',
'advanced' => 'Erweitert',
'archiving' => 'Archivierung',
'auth' => array(
'configuration' => 'Anmelden',
'help' => 'Die Verbindung erlaubt Zugriff auf HTTP-geschützte RSS-Feeds',
'http' => 'HTTP-Authentifizierung',
'password' => 'HTTP-Passwort',
'username' => 'HTTP-Nutzername',
),
'css_help' => 'Ruft gekürzte RSS-Feeds ab (Achtung, benötigt mehr Zeit!)',
'css_path' => 'Pfad zur CSS-Datei des Artikels auf der Original-Webseite',
'description' => 'Beschreibung',
'empty' => 'Dieser Feed ist leer. Bitte stellen Sie sicher, dass er noch gepflegt wird.',
'error' => 'Dieser Feed ist auf ein Problem gestoßen. Bitte stellen Sie sicher, dass er immer lesbar ist und aktualisieren Sie ihn dann.',
'in_main_stream' => 'In Haupt-Feeds zeigen',
'informations' => 'Information',
'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird',
'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie <em>%s</em> eingefügt.',
'no_selected' => 'Kein Feed ausgewählt.',
'number_entries' => '%d Artikel',
'stats' => 'Statistiken',
'think_to_add' => 'Sie können Feeds hinzufügen.',
'title' => 'Titel',
'title_add' => 'Einen RSS-Feed hinzufügen',
'ttl' => 'Aktualisiere automatisch nicht öfter als',
'url' => 'Feed-URL',
'validator' => 'Überprüfen Sie die Gültigkeit des Feeds',
'website' => 'Webseiten-URL',
'pubsubhubbub' => 'Sofortbenachrichtigung mit PubSubHubbub',
),
'import_export' => array(
'export' => 'Exportieren',
'export_opml' => 'Liste der Feeds exportieren (OPML)',
'export_starred' => 'Ihre Favoriten exportieren',
'feed_list' => 'Liste von %s Artikeln',
'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder ZIP)',
'file_to_import_no_zip' => 'Zu importierende Datei<br />(OPML oder JSON)',
'import' => 'Importieren',
'starred_list' => 'Liste der Lieblingsartikel',
'title' => 'Importieren / Exportieren',
),
'menu' => array(
'bookmark' => 'Abonnieren (FreshRSS-Lesezeichen)',
'import_export' => 'Importieren / Exportieren',
'subscription_management' => 'Abonnementverwaltung',
),
'title' => array(
'_' => 'Abonnementverwaltung',
'feed_management' => 'Verwaltung der RSS-Feeds',
),
);

View File

@@ -1,326 +0,0 @@
<?php
return array (
// LAYOUT
'login' => 'Login',
'login_with_persona' => 'Login with Persona',
'logout' => 'Logout',
'search' => 'Search words or #tags',
'search_short' => 'Search',
'configuration' => 'Configuration',
'users' => 'Users',
'categories' => 'Categories',
'category' => 'Category',
'feed' => 'Feed',
'feeds' => 'Feeds',
'shortcuts' => 'Shortcuts',
'about' => 'About',
'stats' => 'Statistics',
'your_rss_feeds' => 'Your RSS feeds',
'add_rss_feed' => 'Add a RSS feed',
'no_rss_feed' => 'No RSS feed',
'import_export_opml' => 'Import / export (OPML)',
'subscription_management' => 'Subscriptions management',
'main_stream' => 'Main stream',
'all_feeds' => 'All feeds',
'favorite_feeds' => 'Favourites (%d)',
'not_read' => '%d unread',
'not_reads' => '%d unread',
'filter' => 'Filter',
'see_website' => 'See website',
'administration' => 'Manage',
'actualize' => 'Actualize',
'mark_read' => 'Mark as read',
'mark_favorite' => 'Mark as favourite',
'mark_all_read' => 'Mark all as read',
'mark_feed_read' => 'Mark feed as read',
'mark_cat_read' => 'Mark category as read',
'before_one_day' => 'Before one day',
'before_one_week' => 'Before one week',
'display' => 'Display',
'normal_view' => 'Normal view',
'reader_view' => 'Reading view',
'global_view' => 'Global view',
'rss_view' => 'RSS feed',
'show_all_articles' => 'Show all articles',
'show_not_reads' => 'Show only unread',
'show_read' => 'Show only read',
'show_favorite' => 'Show favorites',
'older_first' => 'Oldest first',
'newer_first' => 'Newer first',
// Pagination
'first' => 'First',
'previous' => 'Previous',
'next' => 'Next',
'last' => 'Last',
// CONTROLLERS
'article_published_on' => 'This article originally appeared on <a href="%s">%s</a>',
'article_published_on_author' => 'This article originally appeared on <a href="%s">%s</a> by %s',
'access_denied' => 'You dont have permission to access this page',
'page_not_found' => 'You are looking for a page which doesnt exist',
'error_occurred' => 'An error occurred',
'error_occurred_update' => 'Nothing was changed',
'default_category' => 'Uncategorized',
'categories_updated' => 'Categories have been updated',
'categories_management' => 'Categories management',
'feed_updated' => 'Feed has been updated',
'rss_feed_management' => 'RSS feeds management',
'configuration_updated' => 'Configuration has been updated',
'sharing_management' => 'Sharing options management',
'bad_opml_file' => 'Your OPML file is invalid',
'shortcuts_updated' => 'Shortcuts have been updated',
'shortcuts_management' => 'Shortcuts management',
'shortcuts_navigation' => 'Navigation',
'shortcuts_navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.<br/>With the "Alt" modifier, navigation shortcuts apply on categories.',
'shortcuts_article_action' => 'Article actions',
'shortcuts_other_action' => 'Other actions',
'feeds_marked_read' => 'Feeds have been marked as read',
'updated' => 'Modifications have been updated',
'already_subscribed' => 'You have already subscribed to <em>%s</em>',
'feed_added' => 'RSS feed <em>%s</em> has been added',
'feed_not_added' => '<em>%s</em> could not be added',
'internal_problem_feed' => 'The RSS feed could not be added. Check FressRSS logs for details.',
'invalid_url' => 'URL <em>%s</em> is invalid',
'feed_actualized' => '<em>%s</em> has been updated',
'n_feeds_actualized' => '%d feeds have been updated',
'feeds_actualized' => 'RSS feeds have been updated',
'no_feed_actualized' => 'No RSS feed has been updated',
'n_entries_deleted' => '%d articles have been deleted',
'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
'feeds_imported' => 'Your feeds have been imported and will now be updated',
'category_emptied' => 'Category has been emptied',
'feed_deleted' => 'Feed has been deleted',
'feed_validator' => 'Check the validity of the feed',
'optimization_complete' => 'Optimization complete',
'your_rss_feeds' => 'Your RSS feeds',
'your_favorites' => 'Your favourites',
'public' => 'Public',
'invalid_login' => 'Login is invalid',
// VIEWS
'save' => 'Save',
'delete' => 'Delete',
'cancel' => 'Cancel',
'back_to_rss_feeds' => '← Go back to your RSS feeds',
'feeds_moved_category_deleted' => 'When you delete a category, their feeds are automatically classified under <em>%s</em>.',
'category_number' => 'Category n°%d',
'ask_empty' => 'Clear?',
'number_feeds' => '%d feeds',
'can_not_be_deleted' => 'Cannot be deleted',
'add_category' => 'Add a category',
'new_category' => 'New category',
'javascript_for_shortcuts' => 'JavaScript must be enabled in order to use shortcuts',
'javascript_should_be_activated'=> 'JavaScript must be enabled',
'shift_for_all_read' => '+ <code>shift</code> to mark all articles as read',
'see_on_website' => 'See on original website',
'next_article' => 'Skip to the next article',
'last_article' => 'Skip to the last article',
'previous_article' => 'Skip to the previous article',
'first_article' => 'Skip to the first article',
'next_page' => 'Skip to the next page',
'previous_page' => 'Skip to the previous page',
'collapse_article' => 'Collapse',
'auto_share' => 'Share',
'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.',
'file_to_import' => 'File to import',
'import' => 'Import',
'export' => 'Export',
'or' => 'or',
'informations' => 'Information',
'damn' => 'Damn!',
'feed_in_error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
'feed_empty' => 'This feed is empty. Please verify that it is still maintained.',
'feed_description' => 'Description',
'website_url' => 'Website URL',
'feed_url' => 'Feed URL',
'articles' => 'articles',
'number_articles' => 'Number of articles',
'by_feed' => 'by feed',
'by_default' => 'By default',
'keep_history' => 'Minimum number of articles to keep',
'categorize' => 'Store in a category',
'truncate' => 'Delete all articles',
'advanced' => 'Advanced',
'show_in_all_flux' => 'Show in main stream',
'yes' => 'Yes',
'no' => 'No',
'css_path_on_website' => 'Articles CSS path on original website',
'retrieve_truncated_feeds' => 'Retrieves truncated RSS feeds (attention, requires more time!)',
'http_authentication' => 'HTTP Authentication',
'http_username' => 'HTTP username',
'http_password' => 'HTTP password',
'blank_to_disable' => 'Leave blank to disable',
'not_yet_implemented' => 'Not yet implemented',
'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds',
'no_selected_feed' => 'No feed selected.',
'think_to_add' => '<a href="./?c=configure&amp;a=feed">You may add some feeds</a>.',
'current_user' => 'Current user',
'default_user' => 'Username of the default user <small>(maximum 16 alphanumeric characters)</small>',
'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
'persona_connection_email' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)',
'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles',
'auth_token' => 'Authentication token',
'explain_token' => 'Allows to access RSS output of the default user without authentication.<br /><kbd>%s?output=rss&token=%s</kbd>',
'login_configuration' => 'Login',
'is_admin' => 'is administrator',
'auth_type' => 'Authentication method',
'auth_none' => 'None (dangerous)',
'auth_form' => 'Web form (traditional, requires JavaScript)',
'http_auth' => 'HTTP (for advanced users with HTTPS)',
'auth_persona' => 'Mozilla Persona (modern, requires JavaScript)',
'users_list' => 'List of users',
'create_user' => 'Create new user',
'username' => 'Username',
'password' => 'Password',
'create' => 'Create',
'user_created' => 'User %s has been created',
'user_deleted' => 'User %s has been deleted',
'language' => 'Language',
'month' => 'months',
'archiving_configuration' => 'Archiving',
'delete_articles_every' => 'Remove articles after',
'purge_now' => 'Purge now',
'purge_completed' => 'Purge completed (%d articles deleted)',
'archiving_configuration_help' => 'More options are available in the individual stream settings',
'reading_configuration' => 'Reading',
'articles_per_page' => 'Number of articles per page',
'default_view' => 'Default view',
'sort_order' => 'Sort order',
'auto_load_more' => 'Load next articles at the page bottom',
'display_articles_unfolded' => 'Show articles unfolded by default',
'after_onread' => 'After “mark all as read”,',
'jump_next' => 'jump to next unread sibling (feed or category)',
'reading_icons' => 'Reading icons',
'top_line' => 'Top line',
'bottom_line' => 'Bottom line',
'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
'auto_read_when' => 'Mark article as read…',
'article_selected' => 'when article is selected',
'article_open_on_website' => 'when article is opened on its original website',
'scroll' => 'during page scrolls',
'upon_reception' => 'upon reception of the article',
'your_shaarli' => 'Your Shaarli',
'your_wallabag' => 'Your wallabag',
'your_diaspora_pod' => 'Your Diaspora* pod',
'sharing' => 'Sharing',
'share' => 'Share',
'by_email' => 'By email',
'optimize_bdd' => 'Optimize database',
'optimize_todo_sometimes' => 'To do occasionally to reduce the size of the database',
'theme' => 'Theme',
'more_information' => 'More information',
'activate_sharing' => 'Activate sharing',
'shaarli' => 'Shaarli',
'wallabag' => 'wallabag',
'diaspora' => 'Diaspora*',
'twitter' => 'Twitter',
'g+' => 'Google+',
'facebook' => 'Facebook',
'email' => 'Email',
'print' => 'Print',
'article' => 'Article',
'title' => 'Title',
'author' => 'Author',
'publication_date' => 'Date of publication',
'by' => 'by',
'load_more' => 'Load more articles',
'nothing_to_load' => 'There are no more articles',
'rss_feeds_of' => 'RSS feed of %s',
'refresh' => 'Refresh',
'no_feed_to_refresh' => 'There is no feed to refresh…',
'today' => 'Today',
'yesterday' => 'Yesterday',
'before_yesterday' => 'Before yesterday',
'new_article' => 'There are new available articles, click to refresh the page.',
'by_author' => 'By <em>%s</em>',
'related_tags' => 'Related tags',
'no_feed_to_display' => 'There is no article to show.',
'about_freshrss' => 'About FreshRSS',
'project_website' => 'Project website',
'lead_developer' => 'Lead developer',
'website' => 'Website',
'bugs_reports' => 'Bugs reports',
'github_or_email' => '<a href="https://github.com/marienfressinaud/FreshRSS/issues">on Github</a> or <a href="mailto:dev@marienfressinaud.fr">by mail</a>',
'license' => 'License',
'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
'credits' => 'Credits',
'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesnt use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police used has been created by <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons are collected with <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
'version' => 'Version',
'logs' => 'Logs',
'logs_empty' => 'Log file is empty',
'clear_logs' => 'Clear the logs',
'forbidden_access' => 'Access is forbidden!',
'login_required' => 'Login required:',
'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
// DATE
'january' => 'january',
'february' => 'february',
'march' => 'march',
'april' => 'april',
'may' => 'may',
'june' => 'june',
'july' => 'july',
'august' => 'august',
'september' => 'september',
'october' => 'october',
'november' => 'november',
'december' => 'december',
// special format for date() function
'Jan' => '\J\a\n\u\a\r\y',
'Feb' => '\F\e\b\r\u\a\r\y',
'Mar' => '\M\a\r\c\h',
'Apr' => '\A\p\r\i\l',
'May' => '\M\a\y',
'Jun' => '\J\u\n\e',
'Jul' => '\J\u\l\y',
'Aug' => '\A\u\g\u\s\t',
'Sep' => '\S\e\p\t\e\m\b\e\r',
'Oct' => '\O\c\t\o\b\e\r',
'Nov' => '\N\o\v\e\m\b\e\r',
'Dec' => '\D\e\c\e\m\b\e\r',
// format for date() function, %s allows to indicate month in letter
'format_date' => '%s j\<\s\u\p\>S\<\/\s\u\p\> Y',
'format_date_hour' => '%s j\<\s\u\p\>S\<\/\s\u\p\> Y \a\t H\:i',
'status_favorites' => 'Favourites',
'status_read' => 'Read',
'status_unread' => 'Unread',
'status_total' => 'Total',
'stats_entry_repartition' => 'Entries repartition',
'stats_entry_per_day' => 'Entries per day (last 30 days)',
'stats_feed_per_category' => 'Feeds per category',
'stats_entry_per_category' => 'Entries per category',
'stats_top_feed' => 'Top ten feeds',
'stats_entry_count' => 'Entry count',
);

181
app/i18n/en/admin.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
return array(
'auth' => array(
'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)',
'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles',
'api_enabled' => 'Allow <abbr>API</abbr> access <small>(required for mobile apps)</small>',
'form' => 'Web form (traditional, requires JavaScript)',
'http' => 'HTTP (for advanced users with HTTPS)',
'none' => 'None (dangerous)',
'title' => 'Authentication',
'title_reset' => 'Authentication reset',
'token' => 'Authentication token',
'token_help' => 'Allows access to RSS output of the default user without authentication:',
'type' => 'Authentication method',
'unsafe_autologin' => 'Allow unsafe automatic login using the format: ',
),
'check_install' => array(
'cache' => array(
'nok' => 'Check permissions on <em>./data/cache</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on cache directory are good.',
),
'categories' => array(
'nok' => 'Category table is bad configured.',
'ok' => 'Category table is ok.',
),
'connection' => array(
'nok' => 'Connection to the database cannot being established.',
'ok' => 'Connection to the database is ok.',
),
'ctype' => array(
'nok' => 'Cannot find a required library for character type checking (php-ctype).',
'ok' => 'You have the required library for character type checking (ctype).',
),
'curl' => array(
'nok' => 'Cannot find the cURL library (php-curl package).',
'ok' => 'You have the cURL library.',
),
'data' => array(
'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on data directory are good.',
),
'database' => 'Database installation',
'dom' => array(
'nok' => 'Cannot find a required library to browse the DOM (php-xml package).',
'ok' => 'You have the required library to browse the DOM.',
),
'entries' => array(
'nok' => 'Entry table is bad configured.',
'ok' => 'Entry table is ok.',
),
'favicons' => array(
'nok' => 'Check permissions on <em>./data/favicons</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on favicons directory are good.',
),
'feeds' => array(
'nok' => 'Feed table is bad configured.',
'ok' => 'Feed table is ok.',
),
'fileinfo' => array(
'nok' => 'Cannot find the PHP fileinfo library (fileinfo package).',
'ok' => 'You have the fileinfo library.',
),
'files' => 'File installation',
'json' => array(
'nok' => 'Cannot find JSON (php5-json package).',
'ok' => 'You have JSON extension.',
),
'minz' => array(
'nok' => 'Cannot find the Minz framework.',
'ok' => 'You have the Minz framework.',
),
'pcre' => array(
'nok' => 'Cannot find a required library for regular expressions (php-pcre).',
'ok' => 'You have the required library for regular expressions (PCRE).',
),
'pdo' => array(
'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'PHP installation',
'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.',
'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.',
),
'tables' => array(
'nok' => 'There is one or more lacking tables in the database.',
'ok' => 'Tables are existing in the database.',
),
'title' => 'Installation checking',
'tokens' => array(
'nok' => 'Check permissions on <em>./data/tokens</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on tokens directory are good.',
),
'users' => array(
'nok' => 'Check permissions on <em>./data/users</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on users directory are good.',
),
'zip' => array(
'nok' => 'Cannot find ZIP extension (php-zip package).',
'ok' => 'You have ZIP extension.',
),
),
'extensions' => array(
'disabled' => 'Disabled',
'empty_list' => 'There is no installed extension',
'enabled' => 'Enabled',
'no_configure_view' => 'This extension cannot be configured.',
'system' => array(
'_' => 'System extensions',
'no_rights' => 'System extension (you have no rights on it)',
),
'title' => 'Extensions',
'user' => 'User extensions',
),
'stats' => array(
'_' => 'Statistics',
'all_feeds' => 'All feeds',
'category' => 'Category',
'entry_count' => 'Entry count',
'entry_per_category' => 'Entries per category',
'entry_per_day' => 'Entries per day (last 30 days)',
'entry_per_day_of_week' => 'Per day of week (average: %.2f messages)',
'entry_per_hour' => 'Per hour (average: %.2f messages)',
'entry_per_month' => 'Per month (average: %.2f messages)',
'entry_repartition' => 'Entries repartition',
'feed' => 'Feed',
'feed_per_category' => 'Feeds per category',
'idle' => 'Idle feeds',
'main' => 'Main statistics',
'main_stream' => 'Main stream',
'menu' => array(
'idle' => 'Idle feeds',
'main' => 'Main statistics',
'repartition' => 'Articles repartition',
),
'no_idle' => 'There is no idle feed!',
'number_entries' => '%d articles',
'percent_of_total' => '%% of total',
'repartition' => 'Articles repartition',
'status_favorites' => 'Favourites',
'status_read' => 'Read',
'status_total' => 'Total',
'status_unread' => 'Unread',
'title' => 'Statistics',
'top_feed' => 'Top ten feeds',
),
'system' => array(
'_' => 'System configuration',
'auto-update-url' => 'Auto-update server URL',
'instance-name' => 'Instance name',
'max-categories' => 'Categories per user limit',
'max-feeds' => 'Feeds per user limit',
'registration' => array(
'help' => '0 means that there is no account limit',
'number' => 'Max number of accounts',
),
),
'update' => array(
'_' => 'Update system',
'apply' => 'Apply',
'check' => 'Check for new updates',
'current_version' => 'Your current version of FreshRSS is the %s.',
'last' => 'Last verification: %s',
'none' => 'No update to apply',
'title' => 'Update system',
),
'user' => array(
'articles_and_size' => '%s articles (%s)',
'create' => 'Create new user',
'language' => 'Language',
'number' => 'There is %d account created yet',
'numbers' => 'There are %d accounts created yet',
'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
'password_format' => 'At least 7 characters',
'title' => 'Manage users',
'user_list' => 'List of users',
'username' => 'Username',
'users' => 'Users',
),
);

173
app/i18n/en/conf.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
return array(
'archiving' => array(
'_' => 'Archiving',
'advanced' => 'Advanced',
'delete_after' => 'Remove articles after',
'help' => 'More options are available in the individual feed settings',
'keep_history_by_feed' => 'Minimum number of articles to keep by feed',
'optimize' => 'Optimise database',
'optimize_help' => 'To do occasionally to reduce the size of the database',
'purge_now' => 'Purge now',
'title' => 'Archiving',
'ttl' => 'Do not automatically refresh more often than',
),
'display' => array(
'_' => 'Display',
'icon' => array(
'bottom_line' => 'Bottom line',
'entry' => 'Article icons',
'publication_date' => 'Date of publication',
'related_tags' => 'Related tags',
'sharing' => 'Sharing',
'top_line' => 'Top line',
),
'language' => 'Language',
'notif_html5' => array(
'seconds' => 'seconds (0 means no timeout)',
'timeout' => 'HTML5 notification timeout',
),
'theme' => 'Theme',
'title' => 'Display',
'width' => array(
'content' => 'Content width',
'large' => 'Large',
'medium' => 'Medium',
'no_limit' => 'No limit',
'thin' => 'Thin',
),
),
'query' => array(
'_' => 'User queries',
'deprecated' => 'This query is no longer valid. The referenced category or feed has been deleted.',
'filter' => 'Filter applied:',
'get_all' => 'Display all articles',
'get_category' => 'Display "%s" category',
'get_favorite' => 'Display favorite articles',
'get_feed' => 'Display "%s" feed',
'no_filter' => 'No filter',
'none' => 'You havent created any user query yet.',
'number' => 'Query n°%d',
'order_asc' => 'Display oldest articles first',
'order_desc' => 'Display newest articles first',
'search' => 'Search for "%s"',
'state_0' => 'Display all articles',
'state_1' => 'Display read articles',
'state_2' => 'Display unread articles',
'state_3' => 'Display all articles',
'state_4' => 'Display favorite articles',
'state_5' => 'Display read favorite articles',
'state_6' => 'Display unread favorite articles',
'state_7' => 'Display favorite articles',
'state_8' => 'Display not favorite articles',
'state_9' => 'Display read not favorite articles',
'state_10' => 'Display unread not favorite articles',
'state_11' => 'Display not favorite articles',
'state_12' => 'Display all articles',
'state_13' => 'Display read articles',
'state_14' => 'Display unread articles',
'state_15' => 'Display all articles',
'title' => 'User queries',
),
'profile' => array(
'_' => 'Profile management',
'delete' => array(
'_' => 'Account deletion',
'warn' => 'Your account and all the related data will be deleted.',
),
'password_api' => 'API password<br /><small>(e.g., for mobile apps)</small>',
'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
'password_format' => 'At least 7 characters',
'title' => 'Profile',
),
'reading' => array(
'_' => 'Reading',
'after_onread' => 'After “mark all as read”,',
'articles_per_page' => 'Number of articles per page',
'auto_load_more' => 'Load next articles at the page bottom',
'auto_remove_article' => 'Hide articles after reading',
'mark_updated_article_unread' => 'Mark updated articles as unread',
'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions',
'display_articles_unfolded' => 'Show articles unfolded by default',
'display_categories_unfolded' => 'Show categories folded by default',
'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)',
'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
'jump_next' => 'jump to next unread sibling (feed or category)',
'number_divided_when_reader' => 'Divided by 2 in the reading view.',
'read' => array(
'article_open_on_website' => 'when article is opened on its original website',
'article_viewed' => 'when article is viewed',
'scroll' => 'while scrolling',
'upon_reception' => 'upon reception of the article',
'when' => 'Mark article as read…',
),
'show' => array(
'_' => 'Articles to display',
'adaptive' => 'Adjust showing',
'all_articles' => 'Show all articles',
'unread' => 'Show only unread',
),
'sort' => array(
'_' => 'Sort order',
'newer_first' => 'Newer first',
'older_first' => 'Oldest first',
),
'sticky_post' => 'Stick the article to the top when opened',
'title' => 'Reading',
'view' => array(
'default' => 'Default view',
'global' => 'Global view',
'normal' => 'Normal view',
'reader' => 'Reading view',
),
),
'sharing' => array(
'_' => 'Sharing',
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'More information',
'print' => 'Print',
'shaarli' => 'Shaarli',
'share_name' => 'Share name to display',
'share_url' => 'Share URL to use',
'title' => 'Sharing',
'twitter' => 'Twitter',
'wallabag' => 'wallabag',
),
'shortcut' => array(
'_' => 'Shortcuts',
'article_action' => 'Article actions',
'auto_share' => 'Share',
'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.',
'close_dropdown' => 'Close menus',
'collapse_article' => 'Collapse',
'first_article' => 'Skip to the first article',
'focus_search' => 'Access search box',
'help' => 'Display documentation',
'javascript' => 'JavaScript must be enabled in order to use shortcuts',
'last_article' => 'Skip to the last article',
'load_more' => 'Load more articles',
'mark_read' => 'Mark as read',
'mark_favorite' => 'Mark as favourite',
'navigation' => 'Navigation',
'navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.<br/>With the "Alt" modifier, navigation shortcuts apply on categories.',
'next_article' => 'Skip to the next article',
'other_action' => 'Other actions',
'previous_article' => 'Skip to the previous article',
'see_on_website' => 'See on original website',
'shift_for_all_read' => '+ <code>shift</code> to mark all articles as read',
'title' => 'Shortcuts',
'user_filter' => 'Access user filters',
'user_filter_help' => 'If there is only one user filter, it is used. Else filters are accessible by their number.',
),
'user' => array(
'articles_and_size' => '%s articles (%s)',
'current' => 'Current user',
'is_admin' => 'is administrator',
'users' => 'Users',
),
);

109
app/i18n/en/feedback.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
return array(
'admin' => array(
'optimization_complete' => 'Optimisation complete',
),
'access' => array(
'denied' => 'You dont have permission to access this page',
'not_found' => 'You are looking for a page which doesnt exist',
),
'auth' => array(
'form' => array(
'not_set' => 'A problem occured during authentication system configuration. Please retry later.',
'set' => 'Form is now your default authentication system.',
),
'login' => array(
'invalid' => 'Login is invalid',
'success' => 'You are connected',
),
'logout' => array(
'success' => 'You are disconnected',
),
'no_password_set' => 'Administrator password hasnt been set. This feature isnt available.',
),
'conf' => array(
'error' => 'An error occurred during configuration saving',
'query_created' => 'Query "%s" has been created.',
'shortcuts_updated' => 'Shortcuts have been updated',
'updated' => 'Configuration has been updated',
),
'extensions' => array(
'already_enabled' => '%s is already enabled',
'disable' => array(
'ko' => '%s cannot be disabled. <a href="%s">Check FressRSS logs</a> for details.',
'ok' => '%s is now disabled',
),
'enable' => array(
'ko' => '%s cannot be enabled. <a href="%s">Check FressRSS logs</a> for details.',
'ok' => '%s is now enabled',
),
'no_access' => 'You have no access on %s',
'not_enabled' => '%s is not enabled yet',
'not_found' => '%s does not exist',
),
'import_export' => array(
'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
'feeds_imported' => 'Your feeds have been imported and will now be updated',
'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
'file_cannot_be_uploaded' => 'File cannot be uploaded!',
'no_zip_extension' => 'ZIP extension is not present on your server.',
'zip_error' => 'An error occured during ZIP import.',
),
'sub' => array(
'actualize' => 'Actualise',
'category' => array(
'created' => 'Category %s has been created.',
'deleted' => 'Category has been deleted.',
'emptied' => 'Category has been emptied',
'error' => 'Category cannot be updated',
'name_exists' => 'Category name already exists.',
'no_id' => 'You must precise the id of the category.',
'no_name' => 'Category name cannot be empty.',
'not_delete_default' => 'You cannot delete the default category!',
'not_exist' => 'The category does not exist!',
'over_max' => 'You have reached your limit of categories (%d)',
'updated' => 'Category has been updated.',
),
'feed' => array(
'actualized' => '<em>%s</em> has been updated',
'actualizeds' => 'RSS feeds have been updated',
'added' => 'RSS feed <em>%s</em> has been added',
'already_subscribed' => 'You have already subscribed to <em>%s</em>',
'deleted' => 'Feed has been deleted',
'error' => 'Feed cannot be updated',
'internal_problem' => 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.',
'invalid_url' => 'URL <em>%s</em> is invalid',
'marked_read' => 'Feeds have been marked as read',
'n_actualized' => '%d feeds have been updated',
'n_entries_deleted' => '%d articles have been deleted',
'no_refresh' => 'There is no feed to refresh…',
'not_added' => '<em>%s</em> could not be added',
'over_max' => 'You have reached your limit of feeds (%d)',
'updated' => 'Feed has been updated',
),
'purge_completed' => 'Purge completed (%d articles deleted)',
),
'update' => array(
'can_apply' => 'FreshRSS will now be updated to the <strong>version %s</strong>.',
'error' => 'The update process has encountered an error: %s',
'file_is_nok' => 'Check permissions on <em>%s</em> directory. HTTP server must have rights to write into',
'finished' => 'Update completed!',
'none' => 'No update to apply',
'server_not_found' => 'Update server cannot be found. [%s]',
),
'user' => array(
'created' => array(
'_' => 'User %s has been created',
'error' => 'User %s cannot be created',
),
'deleted' => array(
'_' => 'User %s has been deleted',
'error' => 'User %s cannot be deleted',
),
),
'profile' => array(
'error' => 'Your profile cannot be modified',
'updated' => 'Your profile has been modified',
),
);

182
app/i18n/en/gen.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
return array(
'action' => array(
'actualize' => 'Actualize',
'back_to_rss_feeds' => '← Go back to your RSS feeds',
'cancel' => 'Cancel',
'create' => 'Create',
'disable' => 'Disable',
'empty' => 'Empty',
'enable' => 'Enable',
'export' => 'Export',
'filter' => 'Filter',
'import' => 'Import',
'manage' => 'Manage',
'mark_read' => 'Mark as read',
'mark_favorite' => 'Mark as favourite',
'remove' => 'Remove',
'see_website' => 'See website',
'submit' => 'Submit',
'truncate' => 'Delete all articles',
),
'auth' => array(
'email' => 'Email address',
'keep_logged_in' => 'Keep me logged in <small>(%s days)</small>',
'login' => 'Login',
'logout' => 'Logout',
'password' => array(
'_' => 'Password',
'format' => '<small>At least 7 characters</small>',
),
'registration' => array(
'_' => 'New account',
'ask' => 'Create an account?',
'title' => 'Account creation',
),
'reset' => 'Authentication reset',
'username' => array(
'_' => 'Username',
'admin' => 'Administrator username',
'format' => '<small>maximum 16 alphanumeric characters</small>',
),
),
'date' => array(
'Apr' => '\\A\\p\\r\\i\\l',
'Aug' => '\\A\\u\\g\\u\\s\\t',
'Dec' => '\\D\\e\\c\\e\\m\\b\\e\\r',
'Feb' => '\\F\\e\\b\\r\\u\\a\\r\\y',
'Jan' => '\\J\\a\\n\\u\\a\\r\\y',
'Jul' => '\\J\\u\\l\\y',
'Jun' => '\\J\\u\\n\\e',
'Mar' => '\\M\\a\\r\\c\\h',
'May' => '\\M\\a\\y',
'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r',
'Oct' => '\\O\\c\\t\\o\\b\\e\\r',
'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r',
'apr' => 'apr',
'april' => 'Apr',
'aug' => 'aug',
'august' => 'Aug',
'before_yesterday' => 'Before yesterday',
'dec' => 'dec',
'december' => 'Dec',
'feb' => 'feb',
'february' => 'Feb',
'format_date' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y',
'format_date_hour' => '%s j\\<\\s\\u\\p\\>S\\<\\/\\s\\u\\p\\> Y \\a\\t H\\:i',
'fri' => 'Fri',
'jan' => 'jan',
'january' => 'Jan',
'jul' => 'jul',
'july' => 'Jul',
'jun' => 'jun',
'june' => 'Jun',
'last_3_month' => 'Last three months',
'last_6_month' => 'Last six months',
'last_month' => 'Last month',
'last_week' => 'Last week',
'last_year' => 'Last year',
'mar' => 'mar',
'march' => 'Mar',
'may' => 'May',
'mon' => 'Mon',
'month' => 'months',
'nov' => 'nov',
'november' => 'Nov',
'oct' => 'oct',
'october' => 'Oct',
'sat' => 'Sat',
'sep' => 'sep',
'september' => 'Sep',
'sun' => 'Sun',
'thu' => 'Thu',
'today' => 'Today',
'tue' => 'Tue',
'wed' => 'Wed',
'yesterday' => 'Yesterday',
),
'freshrss' => array(
'_' => 'FreshRSS',
'about' => 'About FreshRSS',
),
'js' => array(
'category_empty' => 'Empty category',
'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!',
'feedback' => array(
'body_new_articles' => 'There are %%d new articles to read on FreshRSS.',
'request_failed' => 'A request has failed, it may have been caused by Internet connection problems.',
'title_new_articles' => 'FreshRSS: new articles!',
),
'new_article' => 'There are new available articles, click to refresh the page.',
'should_be_activated' => 'JavaScript must be enabled',
),
'lang' => array(
'cz' => 'Čeština',
'de' => 'Deutsch',
'en' => 'English',
'fr' => 'Français',
'it' => 'Italiano',
'nl' => 'Nederlands',
'ru' => 'Русский',
'tr' => 'Türkçe',
),
'menu' => array(
'about' => 'About',
'admin' => 'Administration',
'archiving' => 'Archiving',
'authentication' => 'Authentication',
'check_install' => 'Installation checking',
'configuration' => 'Configuration',
'display' => 'Display',
'extensions' => 'Extensions',
'logs' => 'Logs',
'queries' => 'User queries',
'reading' => 'Reading',
'search' => 'Search words or #tags',
'sharing' => 'Sharing',
'shortcuts' => 'Shortcuts',
'stats' => 'Statistics',
'system' => 'System configuration',
'update' => 'Update',
'user_management' => 'Manage users',
'user_profile' => 'Profile',
),
'pagination' => array(
'first' => 'First',
'last' => 'Last',
'load_more' => 'Load more articles',
'mark_all_read' => 'Mark all as read',
'next' => 'Next',
'nothing_to_load' => 'There are no more articles',
'previous' => 'Previous',
),
'share' => array(
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'movim' => 'Movim',
'print' => 'Print',
'shaarli' => 'Shaarli',
'twitter' => 'Twitter',
'wallabag' => 'wallabag v1',
'wallabagv2' => 'wallabag v2',
'jdh' => 'Journal du hacker',
),
'short' => array(
'attention' => 'Warning!',
'blank_to_disable' => 'Leave blank to disable',
'by_author' => 'By <em>%s</em>',
'by_default' => 'By default',
'damn' => 'Damn!',
'default_category' => 'Uncategorized',
'no' => 'No',
'not_applicable' => 'Not available',
'ok' => 'Ok!',
'or' => 'or',
'yes' => 'Yes',
),
);

61
app/i18n/en/index.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
return array(
'about' => array(
'_' => 'About',
'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
'bugs_reports' => 'Bugs reports',
'credits' => 'Credits',
'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesnt use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
'license' => 'License',
'project_website' => 'Project website',
'title' => 'About',
'version' => 'Version',
'website' => 'Website',
),
'feed' => array(
'add' => 'You may add some feeds.',
'empty' => 'There is no article to show.',
'rss_of' => 'RSS feed of %s',
'title' => 'Your RSS feeds',
'title_global' => 'Global view',
'title_fav' => 'Your favourites',
),
'log' => array(
'_' => 'Logs',
'clear' => 'Clear the logs',
'empty' => 'Log file is empty',
'title' => 'Logs',
),
'menu' => array(
'about' => 'About FreshRSS',
'add_query' => 'Add a query',
'before_one_day' => 'Before one day',
'before_one_week' => 'Before one week',
'favorites' => 'Favourites (%s)',
'global_view' => 'Global view',
'main_stream' => 'Main stream',
'mark_all_read' => 'Mark all as read',
'mark_cat_read' => 'Mark category as read',
'mark_feed_read' => 'Mark feed as read',
'newer_first' => 'Newer first',
'non-starred' => 'Show all but favorites',
'normal_view' => 'Normal view',
'older_first' => 'Oldest first',
'queries' => 'User queries',
'read' => 'Show only read',
'reader_view' => 'Reading view',
'rss_view' => 'RSS feed',
'search_short' => 'Search',
'starred' => 'Show only favorites',
'stats' => 'Statistics',
'subscription' => 'Subscriptions management',
'unread' => 'Show only unread',
),
'share' => 'Share',
'tag' => array(
'related' => 'Related tags',
),
);

119
app/i18n/en/install.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
return array(
'action' => array(
'finish' => 'Complete installation',
'fix_errors_before' => 'Please fix errors before skipping to the next step.',
'keep_install' => 'Keep previous configuration',
'next_step' => 'Go to the next step',
'reinstall' => 'Reinstall FreshRSS',
),
'auth' => array(
'form' => 'Web form (traditional, requires JavaScript)',
'http' => 'HTTP (for advanced users with HTTPS)',
'none' => 'None (dangerous)',
'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
'password_format' => 'At least 7 characters',
'type' => 'Authentication method',
),
'bdd' => array(
'_' => 'Database',
'conf' => array(
'_' => 'Database configuration',
'ko' => 'Verify your database information.',
'ok' => 'Database configuration has been saved.',
),
'host' => 'Host',
'prefix' => 'Table prefix',
'password' => 'Database password',
'type' => 'Type of database',
'username' => 'Database username',
),
'check' => array(
'_' => 'Checks',
'already_installed' => 'We have detected that FreshRSS is already installed!',
'cache' => array(
'nok' => 'Check permissions on <em>./data/cache</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on cache directory are good.',
),
'ctype' => array(
'nok' => 'Cannot find a required library for character type checking (php-ctype).',
'ok' => 'You have the required library for character type checking (ctype).',
),
'curl' => array(
'nok' => 'Cannot find the cURL library (php-curl package).',
'ok' => 'You have the cURL library.',
),
'data' => array(
'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on data directory are good.',
),
'dom' => array(
'nok' => 'Cannot find a required library to browse the DOM.',
'ok' => 'You have the required library to browse the DOM.',
),
'favicons' => array(
'nok' => 'Check permissions on <em>./data/favicons</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on favicons directory are good.',
),
'fileinfo' => array(
'nok' => 'Cannot find the PHP fileinfo library (fileinfo package).',
'ok' => 'You have the fileinfo library.',
),
'http_referer' => array(
'nok' => 'Please check that you are not altering your HTTP REFERER.',
'ok' => 'Your HTTP REFERER is known and corresponds to your server.',
),
'json' => array(
'nok' => 'Cannot find a recommended library to parse JSON.',
'ok' => 'You have a recommended library to parse JSON.',
),
'minz' => array(
'nok' => 'Cannot find the Minz framework.',
'ok' => 'You have the Minz framework.',
),
'pcre' => array(
'nok' => 'Cannot find a required library for regular expressions (php-pcre).',
'ok' => 'You have the required library for regular expressions (PCRE).',
),
'pdo' => array(
'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.',
'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.',
),
'users' => array(
'nok' => 'Check permissions on <em>./data/users</em> directory. HTTP server must have rights to write into',
'ok' => 'Permissions on users directory are good.',
),
'xml' => array(
'nok' => 'Cannot find the required library to parse XML.',
'ok' => 'You have the required library to parse XML.',
),
),
'conf' => array(
'_' => 'General configuration',
'ok' => 'General configuration has been saved.',
),
'congratulations' => 'Congratulations!',
'default_user' => 'Username of the default user <small>(maximum 16 alphanumeric characters)</small>',
'delete_articles_after' => 'Remove articles after',
'fix_errors_before' => 'Please fix errors before skipping to the next step.',
'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled',
'js' => array(
'confirm_reinstall' => 'You will lose your previous configuration by reinstalling FreshRSS. Are you sure you want to continue?',
),
'language' => array(
'_' => 'Language',
'choose' => 'Choose a language for FreshRSS',
'defined' => 'Language has been defined.',
),
'not_deleted' => 'Something went wrong; you must delete the file <em>%s</em> manually.',
'ok' => 'The installation process was successful.',
'step' => 'step %d',
'steps' => 'Steps',
'title' => 'Installation · FreshRSS',
'this_is_the_end' => 'This is the end',
);

62
app/i18n/en/sub.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
return array(
'category' => array(
'_' => 'Category',
'add' => 'Add a category',
'empty' => 'Empty category',
'new' => 'New category',
),
'feed' => array(
'add' => 'Add a RSS feed',
'advanced' => 'Advanced',
'archiving' => 'Archivage',
'auth' => array(
'configuration' => 'Login',
'help' => 'Connection allows to access HTTP protected RSS feeds',
'http' => 'HTTP Authentication',
'password' => 'HTTP password',
'username' => 'HTTP username',
),
'css_help' => 'Retrieves truncated RSS feeds (caution, requires more time!)',
'css_path' => 'Articles CSS path on original website',
'description' => 'Description',
'empty' => 'This feed is empty. Please verify that it is still maintained.',
'error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
'in_main_stream' => 'Show in main stream',
'informations' => 'Information',
'keep_history' => 'Minimum number of articles to keep',
'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under <em>%s</em>.',
'no_selected' => 'No feed selected.',
'number_entries' => '%d articles',
'stats' => 'Statistics',
'think_to_add' => 'You may add some feeds.',
'title' => 'Title',
'title_add' => 'Add a RSS feed',
'ttl' => 'Do not automatically refresh more often than',
'url' => 'Feed URL',
'validator' => 'Check the validity of the feed',
'website' => 'Website URL',
'pubsubhubbub' => 'Instant notification with PubSubHubbub',
),
'import_export' => array(
'export' => 'Export',
'export_opml' => 'Export list of feeds (OPML)',
'export_starred' => 'Export your favourites',
'feed_list' => 'List of %s articles',
'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)',
'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)',
'import' => 'Import',
'starred_list' => 'List of favourite articles',
'title' => 'Import / export',
),
'menu' => array(
'bookmark' => 'Subscribe (FreshRSS bookmark)',
'import_export' => 'Import / export',
'subscription_management' => 'Subscriptions management',
),
'title' => array(
'_' => 'Subscriptions management',
'feed_management' => 'RSS feeds management',
),
);

View File

@@ -1,326 +0,0 @@
<?php
return array (
// LAYOUT
'login' => 'Connexion',
'login_with_persona' => 'Connexion avec Persona',
'logout' => 'Déconnexion',
'search' => 'Rechercher des mots ou des #tags',
'search_short' => 'Rechercher',
'configuration' => 'Configuration',
'users' => 'Utilisateurs',
'categories' => 'Catégories',
'category' => 'Catégorie',
'feed' => 'Flux',
'feeds' => 'Flux',
'shortcuts' => 'Raccourcis',
'about' => 'À propos',
'stats' => 'Statistiques',
'your_rss_feeds' => 'Vos flux RSS',
'add_rss_feed' => 'Ajouter un flux RSS',
'no_rss_feed' => 'Aucun flux RSS',
'import_export_opml' => 'Importer / exporter (OPML)',
'subscription_management' => 'Gestion des abonnements',
'main_stream' => 'Flux principal',
'all_feeds' => 'Tous les flux',
'favorite_feeds' => 'Favoris (%d)',
'not_read' => '%d non lu',
'not_reads' => '%d non lus',
'filter' => 'Filtrer',
'see_website' => 'Voir le site',
'administration' => 'Gestion',
'actualize' => 'Actualiser',
'mark_read' => 'Marquer comme lu',
'mark_favorite' => 'Mettre en favori',
'mark_all_read' => 'Tout marquer comme lu',
'mark_feed_read' => 'Marquer le flux comme lu',
'mark_cat_read' => 'Marquer la catégorie comme lue',
'before_one_day' => 'Antérieurs à 1 jour',
'before_one_week' => 'Antérieurs à 1 semaine',
'display' => 'Affichage',
'normal_view' => 'Vue normale',
'reader_view' => 'Vue lecture',
'global_view' => 'Vue globale',
'rss_view' => 'Flux RSS',
'show_all_articles' => 'Afficher tous les articles',
'show_not_reads' => 'Afficher les non lus',
'show_read' => 'Afficher les lus',
'show_favorite' => 'Afficher les favoris',
'older_first' => 'Plus anciens en premier',
'newer_first' => 'Plus récents en premier',
// Pagination
'first' => 'Début',
'previous' => 'Précédent',
'next' => 'Suivant',
'last' => 'Fin',
// CONTROLLERS
'article_published_on' => 'Article publié initialement sur <a href="%s">%s</a>',
'article_published_on_author' => 'Article publié initialement sur <a href="%s">%s</a> par %s',
'access_denied' => 'Vous navez pas le droit daccéder à cette page',
'page_not_found' => 'La page que vous cherchez nexiste pas',
'error_occurred' => 'Une erreur est survenue',
'error_occurred_update' => 'Rien na été modifié',
'default_category' => 'Sans catégorie',
'categories_updated' => 'Les catégories ont été mises à jour',
'categories_management' => 'Gestion des catégories',
'feed_updated' => 'Le flux a été mis à jour',
'rss_feed_management' => 'Gestion des flux RSS',
'configuration_updated' => 'La configuration a été mise à jour',
'sharing_management' => 'Gestion des options de partage',
'bad_opml_file' => 'Votre fichier OPML nest pas valide',
'shortcuts_updated' => 'Les raccourcis ont été mis à jour',
'shortcuts_management' => 'Gestion des raccourcis',
'shortcuts_navigation' => 'Navigation',
'shortcuts_navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation sappliquent aux flux.<br/>Avec le modificateur "Alt", les raccourcis de navigation sappliquent aux catégories.',
'shortcuts_article_action' => 'Actions associées à larticle courant',
'shortcuts_other_action' => 'Autres actions',
'feeds_marked_read' => 'Les flux ont été marqués comme lus',
'updated' => 'Modifications enregistrées',
'already_subscribed' => 'Vous êtes déjà abonné à <em>%s</em>',
'feed_added' => 'Le flux <em>%s</em> a bien été ajouté',
'feed_not_added' => '<em>%s</em> na pas pu être ajouté',
'internal_problem_feed' => 'Le flux na pas pu être ajouté. Consulter les logs de FreshRSS pour plus de détails.',
'invalid_url' => 'Lurl <em>%s</em> est invalide',
'feed_actualized' => '<em>%s</em> a été mis à jour',
'n_feeds_actualized' => '%d flux ont été mis à jour',
'feeds_actualized' => 'Les flux ont été mis à jour',
'no_feed_actualized' => 'Aucun flux na pu être mis à jour',
'n_entries_deleted' => '%d articles ont été supprimés',
'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues',
'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés',
'category_emptied' => 'La catégorie a été vidée',
'feed_deleted' => 'Le flux a été supprimé',
'feed_validator' => 'Vérifier la valididé du flux',
'optimization_complete' => 'Optimisation terminée',
'your_rss_feeds' => 'Vos flux RSS',
'your_favorites' => 'Vos favoris',
'public' => 'Public',
'invalid_login' => 'Lidentifiant est invalide',
// VIEWS
'save' => 'Enregistrer',
'delete' => 'Supprimer',
'cancel' => 'Annuler',
'back_to_rss_feeds' => '← Retour à vos flux RSS',
'feeds_moved_category_deleted' => 'Lors de la suppression dune catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
'category_number' => 'Catégorie n°%d',
'ask_empty' => 'Vider ?',
'number_feeds' => '%d flux',
'can_not_be_deleted' => 'Ne peut pas être supprimée',
'add_category' => 'Ajouter une catégorie',
'new_category' => 'Nouvelle catégorie',
'javascript_for_shortcuts' => 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis',
'javascript_should_be_activated'=> 'Le JavaScript doit être activé',
'shift_for_all_read' => '+ <code>shift</code> pour marquer tous les articles comme lus',
'see_on_website' => 'Voir sur le site dorigine',
'next_article' => 'Passer à larticle suivant',
'last_article' => 'Passer au dernier article',
'previous_article' => 'Passer à larticle précédent',
'first_article' => 'Passer au premier article',
'next_page' => 'Passer à la page suivante',
'previous_page' => 'Passer à la page précédente',
'collapse_article' => 'Refermer',
'auto_share' => 'Partager',
'auto_share_help' => 'Si il ny a quun mode de partage, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
'file_to_import' => 'Fichier à importer',
'import' => 'Importer',
'export' => 'Exporter',
'or' => 'ou',
'informations' => 'Informations',
'damn' => 'Arf !',
'feed_in_error' => 'Ce flux a rencontré un problème. Veuillez vérifier quil est toujours accessible puis actualisez-le.',
'feed_empty' => 'Ce flux est vide. Veuillez vérifier quil est toujours maintenu.',
'feed_description' => 'Description',
'website_url' => 'URL du site',
'feed_url' => 'URL du flux',
'articles' => 'articles',
'number_articles' => 'Nombre darticles',
'by_feed' => 'par flux',
'by_default' => 'Par défaut',
'keep_history' => 'Nombre minimum darticles à conserver',
'categorize' => 'Ranger dans une catégorie',
'truncate' => 'Supprimer tous les articles',
'advanced' => 'Avancé',
'show_in_all_flux' => 'Afficher dans le flux principal',
'yes' => 'Oui',
'no' => 'Non',
'css_path_on_website' => 'Sélecteur CSS des articles sur le site dorigine',
'retrieve_truncated_feeds' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)',
'http_authentication' => 'Authentification HTTP',
'http_username' => 'Identifiant HTTP',
'http_password' => 'Mot de passe HTTP',
'blank_to_disable' => 'Laissez vide pour désactiver',
'not_yet_implemented' => 'Pas encore implémenté',
'access_protected_feeds' => 'La connexion permet daccéder aux flux protégés par une authentification HTTP',
'no_selected_feed' => 'Aucun flux sélectionné.',
'think_to_add' => '<a href="./?c=configure&amp;a=feed">Vous pouvez ajouter des flux</a>.',
'current_user' => 'Utilisateur actuel',
'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
'default_user' => 'Nom de lutilisateur par défaut <small>(16 caractères alphanumériques maximum)</small>',
'persona_connection_email' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
'allow_anonymous' => 'Autoriser la lecture anonyme des articles de lutilisateur par défaut (%s)',
'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux',
'auth_token' => 'Jeton didentification',
'explain_token' => 'Permet daccéder à la sortie RSS de lutilisateur par défaut sans besoin de sauthentifier.<br /><kbd>%s?output=rss&token=%s</kbd>',
'login_configuration' => 'Identification',
'is_admin' => 'est administrateur',
'auth_type' => 'Méthode dauthentification',
'auth_none' => 'Aucune (dangereux)',
'auth_form' => 'Formulaire (traditionnel, requiert JavaScript)',
'http_auth' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
'auth_persona' => 'Mozilla Persona (moderne, requiert JavaScript)',
'users_list' => 'Liste des utilisateurs',
'create_user' => 'Créer un nouvel utilisateur',
'username' => 'Nom dutilisateur',
'password' => 'Mot de passe',
'create' => 'Créer',
'user_created' => 'Lutilisateur %s a été créé',
'user_deleted' => 'Lutilisateur %s a été supprimé',
'language' => 'Langue',
'month' => 'mois',
'archiving_configuration' => 'Archivage',
'delete_articles_every' => 'Supprimer les articles après',
'purge_now' => 'Purger maintenant',
'purge_completed' => 'Purge effectuée (%d articles supprimés)',
'archiving_configuration_help' => 'Dautres options sont disponibles dans la configuration individuelle des flux',
'reading_configuration' => 'Lecture',
'articles_per_page' => 'Nombre darticles par page',
'default_view' => 'Vue par défaut',
'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',
'after_onread' => 'Après “marquer tout comme lu”,',
'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)',
'reading_icons' => 'Icônes de lecture',
'top_line' => 'Ligne du haut',
'bottom_line' => 'Ligne du bas',
'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images',
'auto_read_when' => 'Marquer un article comme lu…',
'article_selected' => 'lorsque larticle est sélectionné',
'article_open_on_website' => 'lorsque larticle est ouvert sur le site dorigine',
'scroll' => 'au défilement de la page',
'upon_reception' => 'dès la réception du nouvel article',
'your_shaarli' => 'Votre Shaarli',
'your_wallabag' => 'Votre wallabag',
'your_diaspora_pod' => 'Votre pod Diaspora*',
'sharing' => 'Partage',
'share' => 'Partager',
'by_email' => 'Par courriel',
'optimize_bdd' => 'Optimiser la base de données',
'optimize_todo_sometimes' => 'À faire de temps en temps pour réduire la taille de la BDD',
'theme' => 'Thème',
'more_information' => 'Plus dinformations',
'activate_sharing' => 'Activer le partage',
'shaarli' => 'Shaarli',
'wallabag' => 'wallabag',
'diaspora' => 'Diaspora*',
'twitter' => 'Twitter',
'g+' => 'Google+',
'facebook' => 'Facebook',
'email' => 'Courriel',
'print' => 'Imprimer',
'article' => 'Article',
'title' => 'Titre',
'author' => 'Auteur',
'publication_date' => 'Date de publication',
'by' => 'par',
'load_more' => 'Charger plus darticles',
'nothing_to_load' => 'Fin des articles',
'rss_feeds_of' => 'Flux RSS de %s',
'refresh' => 'Actualisation',
'no_feed_to_refresh' => 'Il ny a aucun flux à actualiser…',
'today' => 'Aujourdhui',
'yesterday' => 'Hier',
'before_yesterday' => 'À partir davant-hier',
'new_article' => 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.',
'by_author' => 'Par <em>%s</em>',
'related_tags' => 'Tags associés',
'no_feed_to_display' => 'Il ny a aucun article à afficher.',
'about_freshrss' => 'À propos de FreshRSS',
'project_website' => 'Site du projet',
'lead_developer' => 'Développeur principal',
'website' => 'Site Internet',
'bugs_reports' => 'Rapports de bugs',
'github_or_email' => '<a href="https://github.com/marienfressinaud/FreshRSS/issues">sur Github</a> ou <a href="mailto:dev@marienfressinaud.fr">par courriel</a>',
'license' => 'Licence',
'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à limage de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.',
'credits' => 'Crédits',
'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS nutilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Les favicons sont récupérés grâce au site <a href="https://getfavicon.appspot.com/">getFavicon</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
'version' => 'Version',
'logs' => 'Logs',
'logs_empty' => 'Les logs sont vides',
'clear_logs' => 'Effacer les logs',
'forbidden_access' => 'Laccès vous est interdit !',
'login_required' => 'Accès protégé par mot de passe :',
'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
// DATE
'january' => 'janvier',
'february' => 'février',
'march' => 'mars',
'april' => 'avril',
'may' => 'mai',
'june' => 'juin',
'july' => 'juillet',
'august' => 'août',
'september' => 'septembre',
'october' => 'octobre',
'november' => 'novembre',
'december' => 'décembre',
// format spécial pour la fonction date()
'Jan' => '\j\a\n\v\i\e\r',
'Feb' => '\f\é\v\r\i\e\r',
'Mar' => '\m\a\r\s',
'Apr' => '\a\v\r\i\l',
'May' => '\m\a\i',
'Jun' => '\j\u\i\n',
'Jul' => '\j\u\i\l\l\e\t',
'Aug' => '\a\o\û\t',
'Sep' => '\s\e\p\t\e\m\b\r\e',
'Oct' => '\o\c\t\o\b\r\e',
'Nov' => '\n\o\v\e\m\b\r\e',
'Dec' => '\d\é\c\e\m\b\r\e',
// format pour la fonction date(), %s permet d'indiquer le mois en toutes lettres
'format_date' => 'j %s Y',
'format_date_hour' => 'j %s Y \à H\:i',
'status_favorites' => 'favoris',
'status_read' => 'lus',
'status_unread' => 'non lus',
'status_total' => 'total',
'stats_entry_repartition' => 'Répartition des articles',
'stats_entry_per_day' => 'Nombre darticles par jour (30 derniers jours)',
'stats_feed_per_category' => 'Flux par catégorie',
'stats_entry_per_category' => 'Articles par catégorie',
'stats_top_feed' => 'Les dix plus gros flux',
'stats_entry_count' => 'Nombre darticles',
);

181
app/i18n/fr/admin.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
return array(
'auth' => array(
'allow_anonymous' => 'Autoriser la lecture anonyme des articles de lutilisateur par défaut (%s)',
'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux',
'api_enabled' => 'Autoriser laccès par <abbr>API</abbr> <small>(nécessaire pour les applis mobiles)</small>',
'form' => 'Formulaire (traditionnel, requiert JavaScript)',
'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
'none' => 'Aucune (dangereux)',
'title' => 'Authentification',
'title_reset' => 'Réinitialisation de lauthentification',
'token' => 'Jeton didentification',
'token_help' => 'Permet daccéder à la sortie RSS de lutilisateur par défaut sans besoin de sauthentifier :',
'type' => 'Méthode dauthentification',
'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ',
),
'check_install' => array(
'cache' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/cache</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire de cache sont bons.',
),
'categories' => array(
'nok' => 'La table category est mal configurée.',
'ok' => 'La table category est bien configurée.',
),
'connection' => array(
'nok' => 'La connexion à la base de données est impossible.',
'ok' => 'La connexion à la base de données est bonne.',
),
'ctype' => array(
'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).',
'ok' => 'Vous disposez de la librairie pour la vérification des types de caractères (ctype).',
),
'curl' => array(
'nok' => 'Impossible de trouver la librairie cURL (paquet php-curl).',
'ok' => 'Vous disposez de la librairie cURL.',
),
'data' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire de data sont bons.',
),
'database' => 'Installation de la base de données',
'dom' => array(
'nok' => 'Impossible de trouver une librairie pour parcourir le DOM (paquet php-xml).',
'ok' => 'Vous disposez de la librairie pour parcourir le DOM.',
),
'entries' => array(
'nok' => 'La table entry est mal configurée.',
'ok' => 'La table entry est bien configurée.',
),
'favicons' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/favicons</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire des favicons sont bons.',
),
'feeds' => array(
'nok' => 'La table feed est mal configurée.',
'ok' => 'La table feed est bien configurée.',
),
'fileinfo' => array(
'nok' => 'Impossible de trouver la librairie PHP fileinfo (paquet fileinfo).',
'ok' => 'Vous disposez de la librairie fileinfo.',
),
'files' => 'Installation des fichiers',
'json' => array(
'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).',
'ok' => 'Vous disposez de lextension JSON.',
),
'minz' => array(
'nok' => 'Vous ne disposez pas de la librairie Minz.',
'ok' => 'Vous disposez du framework Minz',
),
'pcre' => array(
'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).',
'ok' => 'Vous disposez de la librairie pour les expressions régulières (PCRE).',
),
'pdo' => array(
'nok' => 'Vous ne disposez pas de PDO ou dun des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Vous disposez de PDO et dau moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'Installation de PHP',
'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.',
'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.',
),
'tables' => array(
'nok' => 'Impossible de trouver une ou plusieurs tables en base de données.',
'ok' => 'Les tables sont bien présentes en base de données.',
),
'title' => 'Vérification de linstallation',
'tokens' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/tokens</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire des tokens sont bons.',
),
'users' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/users</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
),
'zip' => array(
'nok' => 'Vous ne disposez pas de lextension ZIP (paquet php-zip).',
'ok' => 'Vous disposez de lextension ZIP.',
),
),
'extensions' => array(
'disabled' => 'Désactivée',
'empty_list' => 'Il ny a aucune extension installée.',
'enabled' => 'Activée',
'no_configure_view' => 'Cette extension ne peut pas être configurée.',
'system' => array(
'_' => 'Extensions système',
'no_rights' => 'Extension système (vous navez aucun droit dessus)',
),
'title' => 'Extensions',
'user' => 'Extensions utilisateur',
),
'stats' => array(
'_' => 'Statistiques',
'all_feeds' => 'Tous les flux',
'category' => 'Catégorie',
'entry_count' => 'Nombre darticles',
'entry_per_category' => 'Articles par catégorie',
'entry_per_day' => 'Nombre darticles par jour (30 derniers jours)',
'entry_per_day_of_week' => 'Par jour de la semaine (moyenne : %.2f messages)',
'entry_per_hour' => 'Par heure (moyenne : %.2f messages)',
'entry_per_month' => 'Par mois (moyenne : %.2f messages)',
'entry_repartition' => 'Répartition des articles',
'feed' => 'Flux',
'feed_per_category' => 'Flux par catégorie',
'idle' => 'Flux inactifs',
'main' => 'Statistiques principales',
'main_stream' => 'Flux principal',
'menu' => array(
'idle' => 'Flux inactifs',
'main' => 'Statistiques principales',
'repartition' => 'Répartition des articles',
),
'no_idle' => 'Il ny a aucun flux inactif !',
'number_entries' => '%d articles',
'percent_of_total' => '%% du total',
'repartition' => 'Répartition des articles',
'status_favorites' => 'favoris',
'status_read' => 'lus',
'status_total' => 'total',
'status_unread' => 'non lus',
'title' => 'Statistiques',
'top_feed' => 'Les dix plus gros flux',
),
'system' => array(
'_' => 'Configuration du système',
'auto-update-url' => 'URL du service de mise à jour',
'instance-name' => 'Nom de linstance',
'max-categories' => 'Limite de catégories par utilisateur',
'max-feeds' => 'Limite de flux par utilisateur',
'registration' => array(
'help' => 'Un chiffre de 0 signifie que lon peut créer un nombre infini de comptes',
'number' => 'Nombre max de comptes',
),
),
'update' => array(
'_' => 'Système de mise à jour',
'apply' => 'Appliquer la mise à jour',
'check' => 'Vérifier les mises à jour',
'current_version' => 'Votre version actuelle de FreshRSS est la %s.',
'last' => 'Dernière vérification : %s',
'none' => 'Aucune mise à jour à appliquer',
'title' => 'Système de mise à jour',
),
'user' => array(
'articles_and_size' => '%s articles (%s)',
'create' => 'Créer un nouvel utilisateur',
'language' => 'Langue',
'number' => '%d compte a déjà été créé',
'numbers' => '%d comptes ont déjà été créés',
'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
'password_format' => '7 caractères minimum',
'title' => 'Gestion des utilisateurs',
'user_list' => 'Liste des utilisateurs',
'username' => 'Nom dutilisateur',
'users' => 'Utilisateurs',
),
);

173
app/i18n/fr/conf.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
return array(
'archiving' => array(
'_' => 'Archivage',
'advanced' => 'Avancé',
'delete_after' => 'Supprimer les articles après',
'help' => 'Dautres options sont disponibles dans la configuration individuelle des flux.',
'keep_history_by_feed' => 'Nombre minimum darticles à conserver par flux',
'optimize' => 'Optimiser la base de données',
'optimize_help' => 'À faire de temps en temps pour réduire la taille de la BDD',
'purge_now' => 'Purger maintenant',
'title' => 'Archivage',
'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
),
'display' => array(
'_' => 'Affichage',
'icon' => array(
'bottom_line' => 'Ligne du bas',
'entry' => 'Icônes darticle',
'publication_date' => 'Date de publication',
'related_tags' => 'Tags associés',
'sharing' => 'Partage',
'top_line' => 'Ligne du haut',
),
'language' => 'Langue',
'notif_html5' => array(
'seconds' => 'secondes (0 signifie aucun timeout)',
'timeout' => 'Temps daffichage de la notification HTML5',
),
'theme' => 'Thème',
'title' => 'Affichage',
'width' => array(
'content' => 'Largeur du contenu',
'large' => 'Large',
'medium' => 'Moyenne',
'no_limit' => 'Pas de limite',
'thin' => 'Fine',
),
),
'query' => array(
'_' => 'Filtres utilisateurs',
'deprecated' => 'Ce filtre nest plus valide. La catégorie ou le flux concerné a été supprimé.',
'filter' => 'Filtres appliqués :',
'get_all' => 'Afficher tous les articles',
'get_category' => 'Afficher la catégorie "%s"',
'get_favorite' => 'Afficher les articles favoris',
'get_feed' => 'Afficher le flux "%s"',
'no_filter' => 'Aucun filtre appliqué',
'none' => 'Vous navez pas encore créé de filtre.',
'number' => 'Filtre n°%d',
'order_asc' => 'Afficher les articles les plus anciens en premier',
'order_desc' => 'Afficher les articles les plus récents en premier',
'search' => 'Recherche de "%s"',
'state_0' => 'Afficher tous les articles',
'state_1' => 'Afficher les articles lus',
'state_2' => 'Afficher les articles non lus',
'state_3' => 'Afficher tous les articles',
'state_4' => 'Afficher les articles favoris',
'state_5' => 'Afficher les articles lus et favoris',
'state_6' => 'Afficher les articles non lus et favoris',
'state_7' => 'Afficher les articles favoris',
'state_8' => 'Afficher les articles non favoris',
'state_9' => 'Afficher les articles lus et non favoris',
'state_10' => 'Afficher les articles non lus et non favoris',
'state_11' => 'Afficher les articles non favoris',
'state_12' => 'Afficher tous les articles',
'state_13' => 'Afficher les articles lus',
'state_14' => 'Afficher les articles non lus',
'state_15' => 'Afficher tous les articles',
'title' => 'Filtres utilisateurs',
),
'profile' => array(
'_' => 'Gestion du profil',
'delete' => array(
'_' => 'Suppression du compte',
'warn' => 'Le compte et toutes les données associées vont être supprimées.',
),
'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
'password_format' => '7 caractères minimum',
'title' => 'Profil',
),
'reading' => array(
'_' => 'Lecture',
'after_onread' => 'Après “marquer tout comme lu”,',
'articles_per_page' => 'Nombre darticles par page',
'auto_load_more' => 'Charger les articles suivants en bas de page',
'auto_remove_article' => 'Cacher les articles après lecture',
'mark_updated_article_unread' => 'Marquer les articles mis à jour comme non-lus',
'confirm_enabled' => 'Afficher une confirmation lors des actions “marquer tout comme lu”',
'display_articles_unfolded' => 'Afficher les articles dépliés par défaut',
'display_categories_unfolded' => 'Afficher les catégories pliées par défaut',
'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)',
'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images',
'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)',
'number_divided_when_reader' => 'Divisé par 2 dans la vue de lecture.',
'read' => array(
'article_open_on_website' => 'lorsque larticle est ouvert sur le site dorigine',
'article_viewed' => 'lorsque larticle est affiché',
'scroll' => 'au défilement de la page',
'upon_reception' => 'dès la réception du nouvel article',
'when' => 'Marquer un article comme lu…',
),
'show' => array(
'_' => 'Articles à afficher',
'adaptive' => 'Adapter laffichage',
'all_articles' => 'Afficher tous les articles',
'unread' => 'Afficher les non lus',
),
'sort' => array(
'_' => 'Ordre de tri',
'newer_first' => 'Plus récents en premier',
'older_first' => 'Plus anciens en premier',
),
'sticky_post' => 'Aligner larticle en haut quand il est ouvert',
'title' => 'Lecture',
'view' => array(
'default' => 'Vue par défaut',
'global' => 'Vue globale',
'normal' => 'Vue normale',
'reader' => 'Vue lecture',
),
),
'sharing' => array(
'_' => 'Partage',
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Courriel',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Plus dinformations',
'print' => 'Print',
'shaarli' => 'Shaarli',
'share_name' => 'Nom du partage à afficher',
'share_url' => 'URL du partage à utiliser',
'title' => 'Partage',
'twitter' => 'Twitter',
'wallabag' => 'wallabag',
),
'shortcut' => array(
'_' => 'Raccourcis',
'article_action' => 'Actions associées à larticle courant',
'auto_share' => 'Partager',
'auto_share_help' => 'Sil ny a quun mode de partage, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
'close_dropdown' => 'Fermer les menus',
'collapse_article' => 'Refermer',
'first_article' => 'Passer au premier article',
'focus_search' => 'Accéder à la recherche',
'help' => 'Afficher la documentation',
'javascript' => 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis.',
'last_article' => 'Passer au dernier article',
'load_more' => 'Charger plus darticles',
'mark_read' => 'Marquer comme lu',
'mark_favorite' => 'Mettre en favori',
'navigation' => 'Navigation',
'navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation sappliquent aux flux.<br/>Avec le modificateur "Alt", les raccourcis de navigation sappliquent aux catégories.',
'next_article' => 'Passer à larticle suivant',
'other_action' => 'Autres actions',
'previous_article' => 'Passer à larticle précédent',
'see_on_website' => 'Voir sur le site dorigine',
'shift_for_all_read' => '+ <code>shift</code> pour marquer tous les articles comme lus',
'title' => 'Raccourcis',
'user_filter' => 'Accéder aux filtres utilisateur',
'user_filter_help' => 'Sil ny a quun filtre utilisateur, celui-ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
),
'user' => array(
'articles_and_size' => '%s articles (%s)',
'current' => 'Utilisateur actuel',
'is_admin' => 'est administrateur',
'users' => 'Utilisateurs',
),
);

109
app/i18n/fr/feedback.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
return array(
'admin' => array(
'optimization_complete' => 'Optimisation terminée.',
),
'access' => array(
'denied' => 'Vous navez pas le droit daccéder à cette page !',
'not_found' => 'La page que vous cherchez nexiste pas !',
),
'auth' => array(
'form' => array(
'not_set' => 'Un problème est survenu lors de la configuration de votre système dauthentification. Veuillez réessayer plus tard.',
'set' => 'Le formulaire est désormais votre système dauthentification.',
),
'login' => array(
'invalid' => 'Lidentifiant est invalide',
'success' => 'Vous êtes désormais connecté',
),
'logout' => array(
'success' => 'Vous avez été déconnecté',
),
'no_password_set' => 'Aucun mot de passe administrateur na été précisé. Cette fonctionnalité nest pas disponible.',
),
'conf' => array(
'error' => 'Une erreur est survenue durant la sauvegarde de la configuration',
'query_created' => 'Le filtre "%s" a bien été créé.',
'shortcuts_updated' => 'Les raccourcis ont été mis à jour.',
'updated' => 'La configuration a été mise à jour',
),
'extensions' => array(
'already_enabled' => '%s est déjà activée',
'disable' => array(
'ko' => '%s ne peut pas être désactivée. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.',
'ok' => '%s est désormais désactivée',
),
'enable' => array(
'ko' => '%s ne peut pas être activée. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.',
'ok' => '%s est désormais activée',
),
'no_access' => 'Vous navez aucun accès sur %s',
'not_enabled' => '%s nest pas encore activée',
'not_found' => '%s nexiste pas',
),
'import_export' => array(
'export_no_zip_extension' => 'Lextension ZIP nest pas présente sur votre serveur. Veuillez essayer dexporter les fichiers un par un.',
'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.',
'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues.',
'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé !',
'no_zip_extension' => 'Lextension ZIP nest pas présente sur votre serveur.',
'zip_error' => 'Une erreur est survenue durant limport du fichier ZIP.',
),
'sub' => array(
'actualize' => 'Actualiser',
'category' => array(
'created' => 'La catégorie %s a été créée.',
'deleted' => 'La catégorie a été supprimée.',
'emptied' => 'La catégorie a été vidée.',
'error' => 'La catégorie na pas pu être modifiée',
'name_exists' => 'Une catégorie possède déjà ce nom.',
'no_id' => 'Vous devez préciser lid de la catégorie.',
'no_name' => 'Vous devez préciser un nom pour la catégorie.',
'not_delete_default' => 'Vous ne pouvez pas supprimer la catégorie par défaut !',
'not_exist' => 'Cette catégorie nexiste pas !',
'over_max' => 'Vous avez atteint votre limite de catégories (%d)',
'updated' => 'La catégorie a été mise à jour.',
),
'feed' => array(
'actualized' => '<em>%s</em> a été mis à jour.',
'actualizeds' => 'Les flux ont été mis à jour.',
'added' => 'Le flux <em>%s</em> a bien été ajouté.',
'already_subscribed' => 'Vous êtes déjà abonné à <em>%s</em>',
'deleted' => 'Le flux a été supprimé.',
'error' => 'Une erreur est survenue',
'internal_problem' => 'Le flux ne peut pas être ajouté. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.',
'invalid_url' => 'Lurl <em>%s</em> est invalide.',
'marked_read' => 'Les flux ont été marqués comme lus.',
'n_actualized' => '%d flux ont été mis à jour.',
'n_entries_deleted' => '%d articles ont été supprimés.',
'no_refresh' => 'Il ny a aucun flux à actualiser…',
'not_added' => '<em>%s</em> na pas pu être ajouté.',
'over_max' => 'Vous avez atteint votre limite de flux (%d)',
'updated' => 'Le flux a été mis à jour',
),
'purge_completed' => 'Purge effectuée (%d articles supprimés).',
),
'update' => array(
'can_apply' => 'FreshRSS va maintenant être mis à jour vers la <strong>version %s</strong>.',
'error' => 'La mise à jour a rencontré un problème : %s',
'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire <em>%s</em>. Le serveur HTTP doit être capable décrire dedans',
'finished' => 'La mise à jour est terminée !',
'none' => 'Aucune mise à jour à appliquer',
'server_not_found' => 'Le serveur de mise à jour na pas été trouvé. [%s]',
),
'user' => array(
'created' => array(
'_' => 'Lutilisateur %s a été créé.',
'error' => 'Lutilisateur %s ne peut pas être créé.',
),
'deleted' => array(
'_' => 'Lutilisateur %s a été supprimé.',
'error' => 'Lutilisateur %s ne peut pas être supprimé.',
),
),
'profile' => array(
'error' => 'Votre profil na pas pu être mis à jour',
'updated' => 'Votre profil a été mis à jour',
),
);

182
app/i18n/fr/gen.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
return array(
'action' => array(
'actualize' => 'Actualiser',
'back_to_rss_feeds' => '← Retour à vos flux RSS',
'cancel' => 'Annuler',
'create' => 'Créer',
'disable' => 'Désactiver',
'empty' => 'Vider',
'enable' => 'Activer',
'export' => 'Exporter',
'filter' => 'Filtrer',
'import' => 'Importer',
'manage' => 'Gérer',
'mark_read' => 'Marquer comme lu',
'mark_favorite' => 'Mettre en favori',
'remove' => 'Supprimer',
'see_website' => 'Voir le site',
'submit' => 'Valider',
'truncate' => 'Supprimer tous les articles',
),
'auth' => array(
'email' => 'Adresse courriel',
'keep_logged_in' => 'Rester connecté <small>(%s jours)</small>',
'login' => 'Connexion',
'logout' => 'Déconnexion',
'password' => array(
'_' => 'Mot de passe',
'format' => '<small>7 caractères minimum</small>',
),
'registration' => array(
'_' => 'Nouveau compte',
'ask' => 'Créer un compte ?',
'title' => 'Création de compte',
),
'reset' => 'Réinitialisation de lauthentification',
'username' => array(
'_' => 'Nom dutilisateur',
'admin' => 'Nom dutilisateur administrateur',
'format' => '<small>16 caractères alphanumériques maximum</small>',
),
),
'date' => array(
'Apr' => '\\a\\v\\r\\i\\l',
'Aug' => '\\a\\o\\û\\t',
'Dec' => '\\d\\é\\c\\e\\m\\b\\r\\e',
'Feb' => '\\f\\é\\v\\r\\i\\e\\r',
'Jan' => '\\j\\a\\n\\v\\i\\e\\r',
'Jul' => '\\j\\u\\i\\l\\l\\e\\t',
'Jun' => '\\j\\u\\i\\n',
'Mar' => '\\m\\a\\r\\s',
'May' => '\\m\\a\\i',
'Nov' => '\\n\\o\\v\\e\\m\\b\\r\\e',
'Oct' => '\\o\\c\\t\\o\\b\\r\\e',
'Sep' => '\\s\\e\\p\\t\\e\\m\\b\\r\\e',
'apr' => 'avr.',
'april' => 'avril',
'aug' => 'août',
'august' => 'août',
'before_yesterday' => 'À partir davant-hier',
'dec' => 'déc.',
'december' => 'décembre',
'feb' => 'fév.',
'february' => 'février',
'format_date' => 'j %s Y',
'format_date_hour' => 'j %s Y \\à H\\:i',
'fri' => 'ven.',
'jan' => 'jan.',
'january' => 'janvier',
'jul' => 'jui.',
'july' => 'juillet',
'jun' => 'juin',
'june' => 'juin',
'last_3_month' => 'Depuis les trois derniers mois',
'last_6_month' => 'Depuis les six derniers mois',
'last_month' => 'Depuis le mois dernier',
'last_week' => 'Depuis la semaine dernière',
'last_year' => 'Depuis lannée dernière',
'mar' => 'mar.',
'march' => 'mars',
'may' => 'mai.',
'mon' => 'lun.',
'month' => 'mois',
'nov' => 'nov.',
'november' => 'novembre',
'oct' => 'oct.',
'october' => 'octobre',
'sat' => 'sam.',
'sep' => 'sep.',
'september' => 'septembre',
'sun' => 'dim.',
'thu' => 'jeu.',
'today' => 'Aujourdhui',
'tue' => 'mar.',
'wed' => 'mer.',
'yesterday' => 'Hier',
),
'freshrss' => array(
'_' => 'FreshRSS',
'about' => 'À propos de FreshRSS',
),
'js' => array(
'category_empty' => 'Catégorie vide',
'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
'confirm_action_feed_cat' => 'Êtes-vous sûr(e) de vouloir continuer ? Vous perdrez les favoris et les filtres associés. Cette action ne peut être annulée !',
'feedback' => array(
'body_new_articles' => 'Il y a %%d nouveaux articles à lire sur FreshRSS.',
'request_failed' => 'Une requête a échoué, cela peut être dû à des problèmes de connexion à Internet.',
'title_new_articles' => 'FreshRSS : nouveaux articles !',
),
'new_article' => 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.',
'should_be_activated' => 'Le JavaScript doit être activé.',
),
'lang' => array(
'cz' => 'Čeština',
'de' => 'Deutsch',
'en' => 'English',
'fr' => 'Français',
'it' => 'Italiano',
'nl' => 'Nederlands',
'ru' => 'Русский',
'tr' => 'Türkçe',
),
'menu' => array(
'about' => 'À propos',
'admin' => 'Administration',
'archiving' => 'Archivage',
'authentication' => 'Authentification',
'check_install' => 'Vérification de linstallation',
'configuration' => 'Configuration',
'display' => 'Affichage',
'extensions' => 'Extensions',
'logs' => 'Logs',
'queries' => 'Filtres utilisateurs',
'reading' => 'Lecture',
'search' => 'Rechercher des mots ou des #tags',
'sharing' => 'Partage',
'shortcuts' => 'Raccourcis',
'stats' => 'Statistiques',
'system' => 'Configuration du système',
'update' => 'Mise à jour',
'user_management' => 'Gestion des utilisateurs',
'user_profile' => 'Profil',
),
'pagination' => array(
'first' => 'Début',
'last' => 'Fin',
'load_more' => 'Charger plus darticles',
'mark_all_read' => 'Tout marquer comme lu',
'next' => 'Suivant',
'nothing_to_load' => 'Fin des articles',
'previous' => 'Précédent',
),
'share' => array(
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Courriel',
'facebook' => 'Facebook',
'g+' => 'Google+',
'movim' => 'Movim',
'print' => 'Imprimer',
'shaarli' => 'Shaarli',
'twitter' => 'Twitter',
'wallabag' => 'wallabag v1',
'wallabagv2' => 'wallabag v2',
'jdh' => 'Journal du hacker',
),
'short' => array(
'attention' => 'Attention !',
'blank_to_disable' => 'Laissez vide pour désactiver',
'by_author' => 'Par <em>%s</em>',
'by_default' => 'Par défaut',
'damn' => 'Arf !',
'default_category' => 'Sans catégorie',
'no' => 'Non',
'not_applicable' => 'Non disponible',
'ok' => 'Ok !',
'or' => 'ou',
'yes' => 'Oui',
),
);

61
app/i18n/fr/index.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
return array(
'about' => array(
'_' => 'À propos',
'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
'bugs_reports' => 'Rapports de bugs',
'credits' => 'Crédits',
'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS nutilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à limage de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.',
'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">sur Github</a>',
'license' => 'Licence',
'project_website' => 'Site du projet',
'title' => 'À propos',
'version' => 'Version',
'website' => 'Site Internet',
),
'feed' => array(
'add' => 'Vous pouvez ajouter des flux.',
'empty' => 'Il ny a aucun article à afficher.',
'rss_of' => 'Flux RSS de %s',
'title' => 'Vos flux RSS',
'title_global' => 'Vue globale',
'title_fav' => 'Vos favoris',
),
'log' => array(
'_' => 'Logs',
'clear' => 'Effacer les logs',
'empty' => 'Les logs sont vides.',
'title' => 'Logs',
),
'menu' => array(
'about' => 'À propos de FreshRSS',
'add_query' => 'Créer un filtre',
'before_one_day' => 'Antérieurs à 1 jour',
'before_one_week' => 'Antérieurs à 1 semaine',
'favorites' => 'Favoris (%s)',
'global_view' => 'Vue globale',
'main_stream' => 'Flux principal',
'mark_all_read' => 'Tout marquer comme lu',
'mark_cat_read' => 'Marquer la catégorie comme lue',
'mark_feed_read' => 'Marquer le flux comme lu',
'newer_first' => 'Plus récents en premier',
'non-starred' => 'Afficher tout sauf les favoris',
'normal_view' => 'Vue normale',
'older_first' => 'Plus anciens en premier',
'queries' => 'Filtres utilisateurs',
'read' => 'Afficher les lus',
'reader_view' => 'Vue lecture',
'rss_view' => 'Flux RSS',
'search_short' => 'Rechercher',
'starred' => 'Afficher les favoris',
'stats' => 'Statistiques',
'subscription' => 'Gestion des abonnements',
'unread' => 'Afficher les non lus',
),
'share' => 'Partager',
'tag' => array(
'related' => 'Tags associés',
),
);

119
app/i18n/fr/install.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
return array(
'action' => array(
'finish' => 'Terminer linstallation',
'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à létape suivante.',
'keep_install' => 'Garder lancienne configuration',
'next_step' => 'Passer à létape suivante',
'reinstall' => 'Réinstaller FreshRSS',
),
'auth' => array(
'form' => 'Formulaire (traditionnel, requiert JavaScript)',
'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
'none' => 'Aucune (dangereux)',
'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
'password_format' => '7 caractères minimum',
'type' => 'Méthode dauthentification',
),
'bdd' => array(
'_' => 'Base de données',
'conf' => array(
'_' => 'Configuration de la base de données',
'ko' => 'Vérifiez les informations daccès à la base de données.',
'ok' => 'La configuration de la base de données a été enregistrée.',
),
'host' => 'Hôte',
'password' => 'Mot de passe pour base de données',
'prefix' => 'Préfixe des tables',
'type' => 'Type de base de données',
'username' => 'Nom dutilisateur pour base de données',
),
'check' => array(
'_' => 'Vérifications',
'already_installed' => 'FreshRSS semble avoir déjà été installé !',
'cache' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/cache</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire de cache sont bons.',
),
'ctype' => array(
'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).',
'ok' => 'Vous disposez de la librairie pour la vérification des types de caractères (ctype).',
),
'curl' => array(
'nok' => 'Vous ne disposez pas de cURL (paquet php-curl).',
'ok' => 'Vous disposez de cURL.',
),
'data' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire de data sont bons.',
),
'dom' => array(
'nok' => 'Impossible de trouver une librairie pour parcourir le DOM.',
'ok' => 'Vous disposez de la librairie pour parcourir le DOM.',
),
'favicons' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/favicons</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire des favicons sont bons.',
),
'fileinfo' => array(
'nok' => 'Vous ne disposez pas de PHP fileinfo (paquet fileinfo).',
'ok' => 'Vous disposez de fileinfo.',
),
'http_referer' => array(
'nok' => 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.',
'ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.',
),
'json' => array(
'nok' => 'Impossible de trouver une librairie recommandée pour JSON.',
'ok' => 'Vouz disposez de la librairie recommandée pour JSON.',
),
'minz' => array(
'nok' => 'Vous ne disposez pas de la librairie Minz.',
'ok' => 'Vous disposez du framework Minz',
),
'pcre' => array(
'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).',
'ok' => 'Vous disposez de la librairie pour les expressions régulières (PCRE).',
),
'pdo' => array(
'nok' => 'Vous ne disposez pas de PDO ou dun des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Vous disposez de PDO et dau moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.',
'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.',
),
'users' => array(
'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/users</em>. Le serveur HTTP doit être capable décrire dedans',
'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.',
),
'xml' => array(
'nok' => 'Impossible de trouver une librairie requise pour XML.',
'ok' => 'Vouz disposez de la librairie requise pour XML.',
),
),
'conf' => array(
'_' => 'Configuration générale',
'ok' => 'La configuration générale a été enregistrée.',
),
'congratulations' => 'Félicitations !',
'default_user' => 'Nom de lutilisateur par défaut <small>(16 caractères alphanumériques maximum)</small>',
'delete_articles_after' => 'Supprimer les articles après',
'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à létape suivante.',
'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé',
'js' => array(
'confirm_reinstall' => 'Réinstaller FreshRSS vous fera perdre la configuration précédente. Êtes-vous sûr de vouloir continuer ?',
),
'language' => array(
'_' => 'Langue',
'choose' => 'Choisissez la langue pour FreshRSS',
'defined' => 'La langue a bien été définie.',
),
'not_deleted' => 'Quelque chose sest mal passé, vous devez supprimer le fichier <em>%s</em> à la main.',
'ok' => 'Linstallation sest bien passée.',
'step' => 'étape %d',
'steps' => 'Étapes',
'title' => 'Installation · FreshRSS',
'this_is_the_end' => 'This is the end',
);

62
app/i18n/fr/sub.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
return array(
'category' => array(
'_' => 'Catégorie',
'add' => 'Ajouter une catégorie',
'empty' => 'Catégorie vide',
'new' => 'Nouvelle catégorie',
),
'feed' => array(
'add' => 'Ajouter un flux RSS',
'advanced' => 'Avancé',
'archiving' => 'Archivage',
'auth' => array(
'configuration' => 'Identification',
'help' => 'La connexion permet daccéder aux flux protégés par une authentification HTTP.',
'http' => 'Authentification HTTP',
'password' => 'Mot de passe HTTP',
'username' => 'Identifiant HTTP',
),
'css_help' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)',
'css_path' => 'Sélecteur CSS des articles sur le site dorigine',
'description' => 'Description',
'empty' => 'Ce flux est vide. Veuillez vérifier quil est toujours maintenu.',
'error' => 'Ce flux a rencontré un problème. Veuillez vérifier quil est toujours accessible puis actualisez-le.',
'in_main_stream' => 'Afficher dans le flux principal',
'informations' => 'Informations',
'keep_history' => 'Nombre minimum darticles à conserver',
'moved_category_deleted' => 'Lors de la suppression dune catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
'no_selected' => 'Aucun flux sélectionné.',
'number_entries' => '%d articles',
'stats' => 'Statistiques',
'think_to_add' => 'Vous pouvez ajouter des flux.',
'title' => 'Titre',
'title_add' => 'Ajouter un flux RSS',
'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
'url' => 'URL du flux',
'validator' => 'Vérifier la validité du flux',
'website' => 'URL du site',
'pubsubhubbub' => 'Notification instantanée par PubSubHubbub',
),
'import_export' => array(
'export' => 'Exporter',
'export_opml' => 'Exporter la liste des flux (OPML)',
'export_starred' => 'Exporter les favoris',
'feed_list' => 'Liste des articles de %s',
'file_to_import' => 'Fichier à importer<br />(OPML, JSON ou ZIP)',
'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou JSON)',
'import' => 'Importer',
'starred_list' => 'Liste des articles favoris',
'title' => 'Importer / exporter',
),
'menu' => array(
'bookmark' => 'Sabonner (bookmark FreshRSS)',
'import_export' => 'Importer / exporter',
'subscription_management' => 'Gestion des abonnements',
),
'title' => array(
'_' => 'Gestion des abonnements',
'feed_management' => 'Gestion des flux RSS',
),
);

View File

@@ -1,67 +0,0 @@
<?php
return array (
'freshrss_installation' => 'Installation · FreshRSS',
'freshrss' => 'FreshRSS',
'installation_step' => 'Installation — step %d · FreshRSS',
'steps' => 'Steps',
'checks' => 'Checks',
'general_configuration' => 'General configuration',
'bdd_configuration' => 'Database configuration',
'bdd_type' => 'Type of database',
'version_update' => 'Update',
'this_is_the_end' => 'This is the end',
'ok' => 'Ok!',
'congratulations' => 'Congratulations!',
'attention' => 'Attention!',
'damn' => 'Damn!',
'oops' => 'Oops!',
'next_step' => 'Go to the next step',
'language_defined' => 'Language has been defined.',
'choose_language' => 'Choose a language for FreshRSS',
'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled',
'php_is_ok' => 'Your PHP version is %s, which is compatible with FreshRSS',
'php_is_nok' => 'Your PHP version is %s but FreshRSS requires at least version %s',
'minz_is_ok' => 'You have the Minz framework',
'minz_is_nok' => 'You lack the Minz framework. You should execute <em>build.sh</em> script or <a href="https://github.com/marienfressinaud/MINZ">download it on Github</a> and install in <em>%s</em> directory the content of its <em>/lib</em> directory.',
'curl_is_ok' => 'You have version %s of cURL',
'curl_is_nok' => 'You lack cURL (php5-curl package)',
'pdomysql_is_ok' => 'You have PDO and its driver for MySQL',
'pdomysql_is_nok' => 'You lack PDO or its driver for MySQL (php5-mysql package)',
'dom_is_ok' => 'You have the required library to browse the DOM',
'dom_is_nok' => 'You lack a required library to browse the DOM (php-xml package)',
'pcre_is_ok' => 'You have the required library for regular expressions (PCRE)',
'pcre_is_nok' => 'You lack a required library for regular expressions (php-pcre)',
'ctype_is_ok' => 'You have the required library for character type checking (ctype)',
'ctype_is_nok' => 'You lack a required library for character type checking (php-ctype)',
'cache_is_ok' => 'Permissions on cache directory are good',
'log_is_ok' => 'Permissions on logs directory are good',
'favicons_is_ok' => 'Permissions on favicons directory are good',
'data_is_ok' => 'Permissions on data directory are good',
'persona_is_ok' => 'Permissions on Mozilla Persona directory are good',
'file_is_nok' => 'Check permissions on <em>%s</em> directory. HTTP server must have rights to write into',
'fix_errors_before' => 'Fix errors before skip to the next step.',
'general_conf_is_ok' => 'General configuration has been saved.',
'random_string' => 'Random string',
'change_value' => 'You should change this value by any other',
'base_url' => 'Base URL',
'do_not_change_if_doubt' => 'Dont change if you doubt about it',
'bdd_conf_is_ok' => 'Database configuration has been saved.',
'bdd_conf_is_ko' => 'Verify your database information.',
'host' => 'Host',
'bdd' => 'Database',
'prefix' => 'Table prefix',
'update_start' => 'Start update process',
'update_long' => 'This can take a long time, depending on the size of your database. You may have to wait for this page to time out (~5 minutes) and then refresh this page.',
'update_end' => 'Update process is completed, now you can go to the final step.',
'installation_is_ok' => 'The installation process was successful.<br />The final step will now attempt to delete the <kbd>./p/i/install.php</kbd> file and any database backup created during the update process.<br />You may choose to skip this step and delete <kbd>./p/i/install.php</kbd> manually.',
'finish_installation' => 'Complete installation',
'install_not_deleted' => 'Something went wrong; you must delete the file <em>%s</em> manually.',
);

View File

@@ -1,66 +0,0 @@
<?php
return array (
'freshrss_installation' => 'Installation · FreshRSS',
'freshrss' => 'FreshRSS',
'installation_step' => 'Installation — étape %d · FreshRSS',
'steps' => 'Étapes',
'checks' => 'Vérifications',
'general_configuration' => 'Configuration générale',
'bdd_configuration' => 'Base de données',
'bdd_type' => 'Type de base de données',
'version_update' => 'Mise à jour',
'this_is_the_end' => 'This is the end',
'ok' => 'Ok !',
'congratulations' => 'Félicitations !',
'attention' => 'Attention !',
'damn' => 'Arf !',
'oops' => 'Oups !',
'next_step' => 'Passer à létape suivante',
'language_defined' => 'La langue a bien été définie.',
'choose_language' => 'Choisissez la langue pour FreshRSS',
'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé',
'php_is_ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS',
'php_is_nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s',
'minz_is_ok' => 'Vous disposez du framework Minz',
'minz_is_nok' => 'Vous ne disposez pas de la librairie Minz. Vous devriez exécuter le script <em>build.sh</em> ou bien <a href="https://github.com/marienfressinaud/MINZ">la télécharger sur Github</a> et installer dans le répertoire <em>%s</em> le contenu de son répertoire <em>/lib</em>.',
'curl_is_ok' => 'Vous disposez de cURL dans sa version %s',
'curl_is_nok' => 'Vous ne disposez pas de cURL (paquet php5-curl)',
'pdomysql_is_ok' => 'Vous disposez de PDO et de son driver pour MySQL (paquet php5-mysql)',
'pdomysql_is_nok' => 'Vous ne disposez pas de PDO ou de son driver pour MySQL',
'dom_is_ok' => 'Vous disposez du nécessaire pour parcourir le DOM',
'dom_is_nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml)',
'pcre_is_ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE)',
'pcre_is_nok' => 'Il manque une librairie pour les expressions régulières (php-pcre)',
'ctype_is_ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype)',
'ctype_is_nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype)',
'cache_is_ok' => 'Les droits sur le répertoire de cache sont bons',
'log_is_ok' => 'Les droits sur le répertoire des logs sont bons',
'favicons_is_ok' => 'Les droits sur le répertoire des favicons sont bons',
'data_is_ok' => 'Les droits sur le répertoire de data sont bons',
'persona_is_ok' => 'Les droits sur le répertoire de Mozilla Persona sont bons',
'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire <em>%s</em>. Le serveur HTTP doit être capable décrire dedans',
'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à létape suivante.',
'general_conf_is_ok' => 'La configuration générale a été enregistrée.',
'random_string' => 'Chaîne aléatoire',
'change_value' => 'Vous devriez changer cette valeur par nimporte quelle autre',
'base_url' => 'Base de lURL',
'do_not_change_if_doubt' => 'Laissez tel quel dans le doute',
'bdd_conf_is_ok' => 'La configuration de la base de données a été enregistrée.',
'bdd_conf_is_ko' => 'Vérifiez les informations daccès à la base de données.',
'host' => 'Hôte',
'bdd' => 'Base de données',
'prefix' => 'Préfixe des tables',
'update_start' => 'Lancer la mise à jour',
'update_long' => 'Ce processus peut prendre longtemps, selon la taille de votre base de données. Vous aurez peut-être à attendre que cette page dépasse son temps maximum dexécution (~5 minutes) puis à la recharger.',
'update_end' => 'La mise à jour est terminée, vous pouvez maintenant passer à létape finale.',
'installation_is_ok' => 'Linstallation sest bien passée.<br />La dernière étape va maintenant tenter de supprimer le fichier <kbd>./p/i/install.php</kbd>, ainsi que déventuelles copies de base de données créées durant le processus de mise à jour.<br />Vous pouvez choisir de sauter cette étape et de supprimer <kbd>./p/i/install.php</kbd> manuellement.',
'finish_installation' => 'Terminer linstallation',
'install_not_deleted' => 'Quelque chose sest mal passé, vous devez supprimer le fichier <em>%s</em> à la main.',
);

181
app/i18n/it/admin.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
return array(
'auth' => array(
'allow_anonymous' => 'Consenti la lettura agli utenti anonimi degli articoli dell utente predefinito (%s)',
'allow_anonymous_refresh' => 'Consenti agli utenti anonimi di aggiornare gli articoli',
'api_enabled' => 'Consenti le <abbr>API</abbr> di accesso <small>(richiesto per le app mobili)</small>',
'form' => 'Web form (tradizionale, richiede JavaScript)',
'http' => 'HTTP (per gli utenti avanzati con HTTPS)',
'none' => 'Nessuno (pericoloso)',
'title' => 'Autenticazione',
'title_reset' => 'Reset autenticazione',
'token' => 'Token di autenticazione',
'token_help' => 'Consenti accesso agli RSS dell utente predefinito senza autenticazione:',
'type' => 'Metodo di autenticazione',
'unsafe_autologin' => 'Consenti accesso automatico non sicuro usando il formato: ',
),
'check_install' => array(
'cache' => array(
'nok' => 'Verifica i permessi sulla cartella <em>./data/cache</em>. Il server HTTP deve avere i permessi per scriverci dentro',
'ok' => 'I permessi sulla cartella della cache sono corretti.',
),
'categories' => array(
'nok' => 'La tabella delle categorie ha una configurazione errata.',
'ok' => 'Tabella delle categorie OK.',
),
'connection' => array(
'nok' => 'La connessione al database non può essere stabilita.',
'ok' => 'Connessione al database OK',
),
'ctype' => array(
'nok' => 'Manca una libreria richiesta per il controllo dei caratteri (php-ctype).',
'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).',
),
'curl' => array(
'nok' => 'Manca il supporto per cURL (pacchetto php-curl).',
'ok' => 'Estensione cURL presente.',
),
'data' => array(
'nok' => 'Verifica i permessi sulla cartella <em>./data</em>. Il server HTTP deve avere i permessi per scriverci dentro',
'ok' => 'I permessi sulla cartella data sono corretti.',
),
'database' => 'Installazione database',
'dom' => array(
'nok' => 'Manca una libreria richiesta per leggere DOM (pacchetto php-xml).',
'ok' => 'Libreria richiesta per leggere DOM presente.',
),
'entries' => array(
'nok' => 'La tabella Entry ha una configurazione errata.',
'ok' => 'Tabella Entry OK.',
),
'favicons' => array(
'nok' => 'Verifica i permessi sulla cartella <em>./data/favicons</em>. Il server HTTP deve avere i permessi per scriverci dentro',
'ok' => 'I permessi sulla cartella favicons sono corretti.',
),
'feeds' => array(
'nok' => 'La tabella Feed ha una configurazione errata.',
'ok' => 'Tabella Feed OK.',
),
'fileinfo' => array(
'nok' => 'Manca il supporto per PHP fileinfo (pacchetto fileinfo).',
'ok' => 'Estensione fileinfo presente.',
),
'files' => 'Installazione files',
'json' => array(
'nok' => 'Manca il supoorto a JSON (pacchetto php5-json).',
'ok' => 'Estensione JSON presente.',
),
'minz' => array(
'nok' => 'Manca il framework Minz.',
'ok' => 'Framework Minz presente.',
),
'pcre' => array(
'nok' => 'Manca una libreria richiesta per le regular expressions (php-pcre).',
'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
),
'pdo' => array(
'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'Installazione PHP',
'nok' => 'Versione PHP %s FreshRSS richiede almeno la versione %s.',
'ok' => 'Versione PHP %s, compatibile con FreshRSS.',
),
'tables' => array(
'nok' => 'Rilevate tabelle mancanti nel database.',
'ok' => 'Tutte le tabelle sono presenti nel database.',
),
'title' => 'Verifica installazione',
'tokens' => array(
'nok' => 'Verifica i permessi sulla cartella <em>./data/tokens</em>. Il server HTTP deve avere i permessi per scriverci dentro',
'ok' => 'I permessi sulla cartella tokens sono corretti.',
),
'users' => array(
'nok' => 'Verifica i permessi sulla cartella <em>./data/users</em>. Il server HTTP deve avere i permessi per scriverci dentro',
'ok' => 'I permessi sulla cartella users sono corretti.',
),
'zip' => array(
'nok' => 'Manca estensione ZIP (pacchetto php-zip).',
'ok' => 'Estensione ZIP presente.',
),
),
'extensions' => array(
'disabled' => 'Disabilitata',
'empty_list' => 'Non ci sono estensioni installate',
'enabled' => 'Abilitata',
'no_configure_view' => 'Questa estensioni non può essere configurata.',
'system' => array(
'_' => 'Estensioni di sistema',
'no_rights' => 'Estensione di sistema (non hai i permessi su questo tipo)',
),
'title' => 'Estensioni',
'user' => 'Estensioni utente',
),
'stats' => array(
'_' => 'Statistiche',
'all_feeds' => 'Tutti i feeds',
'category' => 'Categoria',
'entry_count' => 'Articoli',
'entry_per_category' => 'Articoli per categoria',
'entry_per_day' => 'Articoli per giorno (ultimi 30 giorni)',
'entry_per_day_of_week' => 'Per giorno della settimana (media: %.2f articoli)',
'entry_per_hour' => 'Per ora (media: %.2f articoli)',
'entry_per_month' => 'Per mese (media: %.2f articoli)',
'entry_repartition' => 'Ripartizione contenuti',
'feed' => 'Feed',
'feed_per_category' => 'Feeds per categoria',
'idle' => 'Feeds non aggiornati',
'main' => 'Statistiche principali',
'main_stream' => 'Flusso principale',
'menu' => array(
'idle' => 'Feeds non aggiornati',
'main' => 'Statistiche principali',
'repartition' => 'Ripartizione articoli',
),
'no_idle' => 'Non ci sono feed non aggiornati',
'number_entries' => '%d articoli',
'percent_of_total' => '%% del totale',
'repartition' => 'Ripartizione articoli',
'status_favorites' => 'Preferiti',
'status_read' => 'Letti',
'status_total' => 'Totale',
'status_unread' => 'Non letti',
'title' => 'Statistiche',
'top_feed' => 'I migliori 10 feeds',
),
'system' => array(
'_' => 'Configurazione di sistema',
'auto-update-url' => 'Auto-update server URL', // @todo translate
'instance-name' => 'Nome istanza',
'max-categories' => 'Limite categorie per utente',
'max-feeds' => 'Limite feeds per utente',
'registration' => array(
'help' => '0 significa che non esiste limite sui profili',
'number' => 'Numero massimo di profili',
),
),
'update' => array(
'_' => 'Aggiornamento sistema',
'apply' => 'Applica',
'check' => 'Controlla la presenza di nuovi aggiornamenti',
'current_version' => 'FreshRSS versione %s.',
'last' => 'Ultima verifica: %s',
'none' => 'Nessun aggiornamento da applicare',
'title' => 'Aggiorna sistema',
),
'user' => array(
'articles_and_size' => '%s articoli (%s)',
'create' => 'Crea nuovo utente',
'language' => 'Lingua',
'number' => ' %d profilo utente creato',
'numbers' => 'Sono presenti %d profili utente',
'password_form' => 'Password<br /><small>(per il login classico)</small>',
'password_format' => 'Almeno 7 caratteri',
'title' => 'Gestione utenti',
'user_list' => 'Lista utenti',
'username' => 'Nome utente',
'users' => 'Utenti',
),
);

173
app/i18n/it/conf.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
return array(
'archiving' => array(
'_' => 'Archiviazione',
'advanced' => 'Avanzate',
'delete_after' => 'Rimuovi articoli dopo',
'help' => 'Altre opzioni sono disponibili nelle impostazioni dei singoli feed',
'keep_history_by_feed' => 'Numero minimo di articoli da mantenere per feed',
'optimize' => 'Ottimizza database',
'optimize_help' => 'Da fare occasionalmente per ridurre le dimensioni del database',
'purge_now' => 'Cancella ora',
'title' => 'Archiviazione',
'ttl' => 'Non effettuare aggiornamenti per più di',
),
'display' => array(
'_' => 'Visualizzazione',
'icon' => array(
'bottom_line' => 'Barra in fondo',
'entry' => 'Icone degli articoli',
'publication_date' => 'Data di pubblicazione',
'related_tags' => 'Tags correlati',
'sharing' => 'Condivisione',
'top_line' => 'Barra in alto',
),
'language' => 'Lingua',
'notif_html5' => array(
'seconds' => 'secondi (0 significa nessun timeout)',
'timeout' => 'Notifica timeout HTML5',
),
'theme' => 'Tema',
'title' => 'Visualizzazione',
'width' => array(
'content' => 'Larghezza contenuto',
'large' => 'Largo',
'medium' => 'Medio',
'no_limit' => 'Nessun limite',
'thin' => 'Stretto',
),
),
'query' => array(
'_' => 'Ricerche personali',
'deprecated' => 'Questa query non è più valida. La categoria o il feed di riferimento non stati cancellati.',
'filter' => 'Filtro applicato:',
'get_all' => 'Mostra tutti gli articoli',
'get_category' => 'Mostra la categoria "%s" ',
'get_favorite' => 'Mostra articoli preferiti',
'get_feed' => 'Mostra feed "%s" ',
'no_filter' => 'Nessun filtro',
'none' => 'Non hai creato nessuna ricerca personale.',
'number' => 'Ricerca n°%d',
'order_asc' => 'Mostra prima gli articoli più vecchi',
'order_desc' => 'Mostra prima gli articoli più nuovi',
'search' => 'Cerca per "%s"',
'state_0' => 'Mostra tutti gli articoli',
'state_1' => 'Mostra gli articoli letti',
'state_2' => 'Mostra gli articoli non letti',
'state_3' => 'Mostra tutti gli articoli',
'state_4' => 'Mostra gli articoli preferiti',
'state_5' => 'Mostra gli articoli preferiti letti',
'state_6' => 'Mostra gli articoli preferiti non letti',
'state_7' => 'Mostra gli articoli preferiti',
'state_8' => 'Non mostrare gli articoli preferiti',
'state_9' => 'Mostra gli articoli letti non preferiti',
'state_10' => 'Mostra gli articoli non letti e non preferiti',
'state_11' => 'Non mostrare gli articoli preferiti',
'state_12' => 'Mostra tutti gli articoli',
'state_13' => 'Mostra gli articoli letti',
'state_14' => 'Mostra gli articoli non letti',
'state_15' => 'Mostra tutti gli articoli',
'title' => 'Ricerche personali',
),
'profile' => array(
'_' => 'Gestione profili',
'delete' => array(
'_' => 'Cancellazione account',
'warn' => 'Il tuo account e tutti i dati associati saranno cancellati.',
),
'password_api' => 'Password API<br /><small>(e.g., per applicazioni mobili)</small>',
'password_form' => 'Password<br /><small>(per il login classico)</small>',
'password_format' => 'Almeno 7 caratteri',
'title' => 'Profilo',
),
'reading' => array(
'_' => 'Lettura',
'after_onread' => 'Dopo “segna tutto come letto”,',
'articles_per_page' => 'Numero di articoli per pagina',
'auto_load_more' => 'Carica articoli successivi a fondo pagina',
'auto_remove_article' => 'Nascondi articoli dopo la lettura',
'mark_updated_article_unread' => 'Segna articoli aggiornati come non letti',
'confirm_enabled' => 'Mostra una conferma per “segna tutto come letto”',
'display_articles_unfolded' => 'Mostra articoli aperti di predefinito',
'display_categories_unfolded' => 'Mostra categorie aperte di predefinito',
'hide_read_feeds' => 'Nascondi categorie e feeds con articoli già letti (non funziona se “Mostra tutti gli articoli” è selezionato)',
'img_with_lazyload' => 'Usa la modalità "caricamento ritardato" per le immagini',
'jump_next' => 'Salta al successivo feed o categoria non letto',
'number_divided_when_reader' => 'Diviso 2 nella modalità di lettura.',
'read' => array(
'article_open_on_website' => 'Quando un articolo è aperto nel suo sito di origine',
'article_viewed' => 'Quando un articolo viene letto',
'scroll' => 'Scorrendo la pagina',
'upon_reception' => 'Alla ricezione del contenuto',
'when' => 'Segna articoli come letti…',
),
'show' => array(
'_' => 'Articoli da visualizzare',
'adaptive' => 'Adatta visualizzazione',
'all_articles' => 'Mostra tutti gli articoli',
'unread' => 'Mostra solo non letti',
),
'sort' => array(
'_' => 'Ordinamento',
'newer_first' => 'Prima i più recenti',
'older_first' => 'Prima i più vecchi',
),
'sticky_post' => 'Blocca il contenuto a inizio pagina quando aperto',
'title' => 'Lettura',
'view' => array(
'default' => 'Visualizzazione predefinita',
'global' => 'Vista globale per categorie',
'normal' => 'Vista elenco',
'reader' => 'Modalità di lettura',
),
),
'sharing' => array(
'_' => 'Condivisione',
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => 'Ulteriori informazioni',
'print' => 'Stampa',
'shaarli' => 'Shaarli',
'share_name' => 'Nome condivisione',
'share_url' => 'URL condivisione',
'title' => 'Condividi',
'twitter' => 'Twitter',
'wallabag' => 'wallabag',
),
'shortcut' => array(
'_' => 'Comandi tastiera',
'article_action' => 'Azioni sugli articoli',
'auto_share' => 'Condividi',
'auto_share_help' => 'Se è presente un solo servizio di condivisione verrà usato quello, altrimenti usare anche il numero associato.',
'close_dropdown' => 'Chiudi menù',
'collapse_article' => 'Collassa articoli',
'first_article' => 'Salta al primo articolo',
'focus_search' => 'Modulo di ricerca',
'help' => 'Mostra documentazione',
'javascript' => 'JavaScript deve essere abilitato per poter usare i comandi da tastiera',
'last_article' => 'Salta all ultimo articolo',
'load_more' => 'Carica altri articoli',
'mark_read' => 'Segna come letto',
'mark_favorite' => 'Segna come preferito',
'navigation' => 'Navigazione',
'navigation_help' => 'Con il tasto "Shift" i comandi di navigazione verranno applicati ai feeds.<br/>Con il tasto "Alt" i comandi di navigazione verranno applicati alle categorie.',
'next_article' => 'Salta al contenuto successivo',
'other_action' => 'Altre azioni',
'previous_article' => 'Salta al contenuto precedente',
'see_on_website' => 'Vai al sito fonte',
'shift_for_all_read' => '+ <code>shift</code> per segnare tutti gli articoli come letti',
'title' => 'Comandi da tastiera',
'user_filter' => 'Accedi alle ricerche personali',
'user_filter_help' => 'Se è presente una sola ricerca personale verrà usata quella, altrimenti usare anche il numero associato.',
),
'user' => array(
'articles_and_size' => '%s articoli (%s)',
'current' => 'Utente connesso',
'is_admin' => 'è amministratore',
'users' => 'Utenti',
),
);

109
app/i18n/it/feedback.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
return array(
'admin' => array(
'optimization_complete' => 'Ottimizzazione completata',
),
'access' => array(
'denied' => 'Non hai i permessi per accedere a questa pagina',
'not_found' => 'Pagina non disponibile',
),
'auth' => array(
'form' => array(
'not_set' => 'Si è verificato un problema alla configurazione del sistema di autenticazione. Per favore riprova più tardi.',
'set' => 'Sistema di autenticazione tramite Form impostato come predefinito.',
),
'login' => array(
'invalid' => 'Autenticazione non valida',
'success' => 'Autenticazione effettuata',
),
'logout' => array(
'success' => 'Disconnessione effettuata',
),
'no_password_set' => 'Password di amministrazione non impostata. Opzione non disponibile.',
),
'conf' => array(
'error' => 'Si è verificato un errore durante il salvataggio della configurazione',
'query_created' => 'Ricerca "%s" creata.',
'shortcuts_updated' => 'Collegamenti tastiera aggiornati',
'updated' => 'Configurazione aggiornata',
),
'extensions' => array(
'already_enabled' => '%s è già abilitata',
'disable' => array(
'ko' => '%s non può essere disabilitata. <a href="%s">Verifica i logs</a> per dettagli.',
'ok' => '%s è disabilitata',
),
'enable' => array(
'ko' => '%s non può essere abilitata. <a href="%s">Verifica i logs</a> per dettagli.',
'ok' => '%s è ora abilitata',
),
'no_access' => 'Accesso negato a %s',
'not_enabled' => '%s non abilitato',
'not_found' => '%s non disponibile',
),
'import_export' => array(
'export_no_zip_extension' => 'Estensione ZIP non presente sul server. Per favore esporta i files singolarmente.',
'feeds_imported' => 'I tuoi feed sono stati importati e saranno aggiornati',
'feeds_imported_with_errors' => 'I tuoi feeds sono stati importati ma si sono verificati alcuni errori',
'file_cannot_be_uploaded' => 'Il file non può essere caricato!',
'no_zip_extension' => 'Estensione ZIP non presente sul server.',
'zip_error' => 'Si è verificato un errore importando il file ZIP',
),
'sub' => array(
'actualize' => 'Aggiorna',
'category' => array(
'created' => 'Categoria %s creata.',
'deleted' => 'Categoria cancellata',
'emptied' => 'Categoria svuotata',
'error' => 'Categoria non aggiornata',
'name_exists' => 'Categoria già esistente.',
'no_id' => 'Categoria senza ID.',
'no_name' => 'Il nome della categoria non può essere lasciato vuoto.',
'not_delete_default' => 'Non puoi cancellare la categoria predefinita!',
'not_exist' => 'La categoria non esite!',
'over_max' => 'Hai raggiunto il numero limite di categorie (%d)',
'updated' => 'Categoria aggiornata.',
),
'feed' => array(
'actualized' => '<em>%s</em> aggiornato',
'actualizeds' => 'RSS feeds aggiornati',
'added' => 'RSS feed <em>%s</em> aggiunti',
'already_subscribed' => 'Hai già sottoscritto <em>%s</em>',
'deleted' => 'Feed cancellato',
'error' => 'Feed non aggiornato',
'internal_problem' => 'RSS feed non aggiunto. <a href="%s">Verifica i logs</a> per dettagli.',
'invalid_url' => 'URL <em>%s</em> non valido',
'marked_read' => 'Feeds segnati come letti',
'n_actualized' => '%d feeds aggiornati',
'n_entries_deleted' => '%d articoli cancellati',
'no_refresh' => 'Nessun aggiornamento disponibile…',
'not_added' => '<em>%s</em> non può essere aggiunto',
'over_max' => 'Hai raggiunto il numero limite di feed (%d)',
'updated' => 'Feed aggiornato',
),
'purge_completed' => 'Svecchiamento completato (%d articoli cancellati)',
),
'update' => array(
'can_apply' => 'FreshRSS verrà aggiornato alla <strong>versione %s</strong>.',
'error' => 'Il processo di aggiornamento ha riscontrato il seguente errore: %s',
'file_is_nok' => 'Verifica i permessi della cartella <em>%s</em>. Il server HTTP deve avere i permessi per la scrittura ',
'finished' => 'Aggiornamento completato con successo!',
'none' => 'Nessun aggiornamento disponibile',
'server_not_found' => 'Server per aggiornamento non disponibile. [%s]',
),
'user' => array(
'created' => array(
'_' => 'Utente %s creato',
'error' => 'Errore nella creazione utente %s ',
),
'deleted' => array(
'_' => 'Utente %s cancellato',
'error' => 'Utente %s non cancellato',
),
),
'profile' => array(
'error' => 'Il tuo profilo non può essere modificato',
'updated' => 'Il tuo profilo è stato modificato',
),
);

182
app/i18n/it/gen.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
return array(
'action' => array(
'actualize' => 'Aggiorna',
'back_to_rss_feeds' => '← Indietro',
'cancel' => 'Annulla',
'create' => 'Crea',
'disable' => 'Disabilita',
'empty' => 'Vuoto',
'enable' => 'Abilita',
'export' => 'Esporta',
'filter' => 'Filtra',
'import' => 'Importa',
'manage' => 'Gestisci',
'mark_read' => 'Segna come letto',
'mark_favorite' => 'Segna come preferito',
'remove' => 'Rimuovi',
'see_website' => 'Vai al sito',
'submit' => 'Conferma',
'truncate' => 'Cancella tutti gli articoli',
),
'auth' => array(
'email' => 'Indirizzo email',
'keep_logged_in' => 'Ricorda i dati <small>(%s giorni)</small>',
'login' => 'Accedi',
'logout' => 'Esci',
'password' => array(
'_' => 'Password',
'format' => '<small>almeno 7 caratteri</small>',
),
'registration' => array(
'_' => 'Nuovo profilo',
'ask' => 'Vuoi creare un nuovo profilo?',
'title' => 'Creazione profilo',
),
'reset' => 'Reset autenticazione',
'username' => array(
'_' => 'Username',
'admin' => 'Username amministratore',
'format' => '<small>massimo 16 caratteri alfanumerici</small>',
),
),
'date' => array(
'Apr' => '\\A\\p\\r\\i\\l\\e',
'Aug' => '\\A\\g\\o\\s\\t\\o',
'Dec' => '\\D\\i\\c\\e\\m\\b\\r\\e',
'Feb' => '\\F\\e\\b\\b\\r\\a\\i\\o',
'Jan' => '\\G\\e\\n\\u\\a\\i\\o',
'Jul' => '\\L\\u\\g\\l\\i\\o',
'Jun' => '\\G\\i\\u\\g\\n\\o',
'Mar' => '\\M\\a\\r\\z\\o',
'May' => '\\M\\a\\g\\g\\i\\o',
'Nov' => '\\N\\o\\v\\e\\m\\b\\r\\e',
'Oct' => '\\O\\t\\t\\o\\b\\r\\e',
'Sep' => '\\S\\e\\t\\t\\e\\m\\b\\r\\e',
'apr' => 'apr',
'april' => 'Apr',
'aug' => 'aug',
'august' => 'Aug',
'before_yesterday' => 'Meno recenti',
'dec' => 'dec',
'december' => 'Dec',
'feb' => 'feb',
'february' => 'Feb',
'format_date' => 'j\\ %s Y',
'format_date_hour' => 'j\\ %s Y \\o\\r\\e H\\:i',
'fri' => 'Fri',
'jan' => 'jan',
'january' => 'Jan',
'jul' => 'jul',
'july' => 'Jul',
'jun' => 'jun',
'june' => 'Jun',
'last_3_month' => 'Ultimi 3 mesi',
'last_6_month' => 'Ultimi 6 mesi',
'last_month' => 'Ultimo mese',
'last_week' => 'Ultima settimana',
'last_year' => 'Ultimo anno',
'mar' => 'mar',
'march' => 'Mar',
'may' => 'May',
'mon' => 'Mon',
'month' => 'mesi',
'nov' => 'nov',
'november' => 'Nov',
'oct' => 'oct',
'october' => 'Oct',
'sat' => 'Sat',
'sep' => 'sep',
'september' => 'Sep',
'sun' => 'Sun',
'thu' => 'Thu',
'today' => 'Oggi',
'tue' => 'Tue',
'wed' => 'Wed',
'yesterday' => 'Ieri',
),
'freshrss' => array(
'_' => 'Feed RSS Reader',
'about' => 'Informazioni',
),
'js' => array(
'category_empty' => 'Categoria vuota',
'confirm_action' => 'Sei sicuro di voler continuare?',
'confirm_action_feed_cat' => 'Sei sicuro di voler continuare? Verranno persi i preferiti e le ricerche utente correlate!',
'feedback' => array(
'body_new_articles' => 'Ci sono %%d nuovi articoli da leggere.',
'request_failed' => 'Richiesta fallita, probabilmente a causa di problemi di connessione',
'title_new_articles' => 'Feed RSS Reader: nuovi articoli!',
),
'new_article' => 'Sono disponibili nuovi articoli, clicca qui per caricarli.',
'should_be_activated' => 'JavaScript deve essere abilitato',
),
'lang' => array(
'cz' => 'Čeština',
'de' => 'Deutsch',
'en' => 'English',
'fr' => 'Français',
'it' => 'Italiano',
'nl' => 'Nederlands',
'ru' => 'Русский',
'tr' => 'Türkçe',
),
'menu' => array(
'about' => 'Informazioni',
'admin' => 'Amministrazione',
'archiving' => 'Archiviazione',
'authentication' => 'Autenticazione',
'check_install' => 'Installazione',
'configuration' => 'Configurazione',
'display' => 'Visualizzazione',
'extensions' => 'Estensioni',
'logs' => 'Logs',
'queries' => 'Ricerche personali',
'reading' => 'Lettura',
'search' => 'Ricerca parole o #tags',
'sharing' => 'Condivisione',
'shortcuts' => 'Comandi tastiera',
'stats' => 'Statistiche',
'system' => 'Configurazione sistema',
'update' => 'Aggiornamento',
'user_management' => 'Gestione utenti',
'user_profile' => 'Profilo',
),
'pagination' => array(
'first' => 'Prima',
'last' => 'Ultima',
'load_more' => 'Carica altri articoli',
'mark_all_read' => 'Segna tutto come letto',
'next' => 'Successiva',
'nothing_to_load' => 'Non ci sono altri articoli',
'previous' => 'Precedente',
),
'share' => array(
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'movim' => 'Movim',
'print' => 'Stampa',
'shaarli' => 'Shaarli',
'twitter' => 'Twitter',
'wallabag' => 'wallabag v1',
'wallabagv2' => 'wallabag v2',
'jdh' => 'Journal du hacker',
),
'short' => array(
'attention' => 'Attenzione!',
'blank_to_disable' => 'Lascia vuoto per disabilitare',
'by_author' => 'di <em>%s</em>',
'by_default' => 'predefinito',
'damn' => 'Ops!',
'default_category' => 'Senza categoria',
'no' => 'No',
'not_applicable' => 'Non disponibile',
'ok' => 'OK!',
'or' => 'o',
'yes' => 'Si',
),
);

Some files were not shown because too many files have changed in this diff Show More