1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/freshrss_ynh.git synced 2024-09-03 18:36:33 +02:00

Initial Commit

This commit is contained in:
plopoyop 2014-07-23 15:52:50 +02:00
commit c441bb3666
287 changed files with 52283 additions and 0 deletions

25
conf/config.php Normal file
View file

@ -0,0 +1,25 @@
<?php
return array (
'general' =>
array (
'environment' => 'production',
'salt' => 'yunosalt',
'base_url' => 'yunopath',
'title' => 'FreshRSS',
'default_user' => 'yunoadminuser',
'allow_anonymous' => false,
'allow_anonymous_refresh' => false,
'auth_type' => 'http',
'api_enabled' => true,
'unsafe_autologin_enabled' => false,
),
'db' =>
array (
'type' => 'mysql',
'host' => 'localhost',
'user' => 'yunouser',
'password' => 'yunopass',
'base' => 'yunobase',
'prefix' => false,
),
);

61
conf/dist_user.conf Normal file
View file

@ -0,0 +1,61 @@
<?php
return array (
'language' => 'en',
'old_entries' => 3,
'keep_history_default' => 0,
'ttl_default' => 3600,
'mail_login' => 'ynoUserEmail',
'token' => '',
'passwordHash' => '',
'apiPasswordHash' => '',
'posts_per_page' => 20,
'view_mode' => 'normal',
'default_view' => 2,
'auto_load_more' => true,
'display_posts' => true,
'onread_jump_next' => true,
'lazyload' => true,
'sticky_post' => true,
'reading_confirm' => false,
'sort_order' => 'DESC',
'anon_access' => false,
'mark_when' =>
array (
'article' => true,
'site' => true,
'scroll' => false,
'reception' => false,
),
'theme' => 'Flat',
'content_width' => 'thin',
'shortcuts' =>
array (
'mark_read' => 'r',
'mark_favorite' => 'f',
'go_website' => 'space',
'next_entry' => 'n',
'prev_entry' => 'k',
'first_entry' => 'home',
'last_entry' => 'end',
'collapse_entry' => 'c',
'load_more' => 'm',
'auto_share' => 's',
'focus_search' => 'a',
),
'topline_read' => true,
'topline_favorite' => true,
'topline_date' => true,
'topline_link' => true,
'bottomline_read' => true,
'bottomline_favorite' => true,
'bottomline_sharing' => true,
'bottomline_tags' => true,
'bottomline_date' => true,
'bottomline_link' => true,
'sharing' =>
array (),
'queries' =>
array (
),
'user' => 'YnoUser',
);

20
conf/nginx.conf Normal file
View file

@ -0,0 +1,20 @@
location PATHTOCHANGE {
alias ALIASTOCHANGE;
if ($scheme = http) {
rewrite ^ https://$server_name$request_uri? permanent;
}
index index.php;
try_files $uri $uri/ /index.php?$args;
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param AUTH_USER $remote_user;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# Include SSOWAT user panel.
include conf.d/yunohost_panel.conf.inc;
}

42
manifest.json Normal file
View file

@ -0,0 +1,42 @@
{
"name": "FreshRSS",
"id": "freshrss",
"description": {
"en": "FreshRSS is a selfhostable RSS reader",
"fr": "FreshRSS est un agrégateur de flux RSS à auto-héberger"
},
"developer": {
"name": "plopoyop",
"email": "plopoyop@gmail.com"
},
"multi_instance": "true",
"arguments": {
"install" : [
{
"name": "domain",
"ask": {
"en": "Choose a domain for FreshRSS",
"fr": "Choisissez un domaine pour FreshRSS"
},
"example": "domain.org"
},
{
"name": "path",
"ask": {
"en": "Choose a path for FreshRSS",
"fr": "Choisissez un chemin pour FreshRSS"
},
"example": "/rss",
"default": "/rss"
}
{
"name": "admin",
"ask": {
"en": "Choose the default user (must be an existing YunoHost user)"
"fr": "Choisissez l'utilisateur par defaut (doit être un utiliser YunoHost existant)"
},
"example": "homer"
}
]
}
}

81
scripts/install Executable file
View file

@ -0,0 +1,81 @@
#!/bin/bash
# Retrieve arguments
domain=$1
path=$2
admin_user=$3
# Check domain/path availability
sudo yunohost app checkurl $domain$path
if [[ ! $? -eq 0 ]]; then
exit 1
fi
# Generate random DES key & password
deskey=$(dd if=/dev/urandom bs=1 count=200 2> /dev/null | tr -c -d 'A-Za-z0-9' | sed -n 's/\(.\{24\}\).*/\1/p')
db_pwd=$(dd if=/dev/urandom bs=1 count=200 2> /dev/null | tr -c -d 'A-Za-z0-9' | sed -n 's/\(.\{24\}\).*/\1/p')
app_salt=$(dd if=/dev/urandom bs=1 count=200 2> /dev/null | tr -c -d 'A-Za-z0-9' | sed -n 's/\(.\{40\}\).*/\1/p')
# Use 'freshrss' as database name and user
db_user=freshrss
# Initialize database and store mysql password for upgrade
sudo yunohost app initdb $db_user -p $db_pwd
sudo yunohost app setting freshrss mysqlpwd -v $db_pwd
# Copy files to the right place
final_path=/var/www/freshrss
sudo mkdir -p $final_path
sudo cp -a ../sources/* $final_path
sudo cp -a ../conf/config.php $final_path/data
# Change variables in freshrss configuration
sudo sed -i "s/yunouser/$db_user/g" $final_path/data/config.php
sudo sed -i "s/yunopass/$db_pwd/g" $final_path/data/config.php
sudo sed -i "s/yunobase/$db_user/g" $final_path/data/config.php
sudo sed -i "s/yunosalt/$app_salt/g" $final_path/data/config.php
sudo sed -i "s/yunopath/$path/g" $final_path/data/config.php
sudo sed -i "s/yunoadminuser/$admin_user/g" $final_path/data/config.php
# Add users
freshrss_users=$(ldapsearch -h localhost -b ou=users,dc=yunohost,dc=org -x objectClass=mailAccount uid | grep uid: | sed 's/uid: //' | xargs)
for myuser in $freshrss_users
do
#copy sql
sudo cp ../sources/app/SQL/install_ynh.sql /tmp/$myuser-install.sql
#change username in sql
sudo sed -i "s/YnoUser/$myuser/g" /tmp/$myuser-install.sql
#create tables
mysql -u $db_user -p$db_pwd $db_user < /tmp/$myuser-install.sql
#remove temp sql
sudo rm /tmp/$myuser-install.sql
#copy default conf
sudo cp ../conf/dist_user.conf $final_path/data/$myuser\_user.php
#change email
user_email=$(ldapsearch -h localhost -b uid=$myuser,ou=users,dc=yunohost,dc=org -x objectClass=mailAccount mail | grep mail: | grep $myuser| sed 's/mail: //')
sudo sed -i "s/ynoUserEmail/$user_email/g" $final_path/data/$myuser\_user.php
#change username
sudo sed -i "s/YnoUser/$myuser/g" $final_path/data/$myuser\_user.php
done
# Delete install directive
sudo rm $final_path/data/do-install.txt
# Modify Nginx configuration file and copy it to Nginx conf directory
sed -i "s@PATHTOCHANGE@$path@g" ../conf/nginx.conf
sed -i "s@ALIASTOCHANGE@$final_path/@g" ../conf/nginx.conf
sudo cp ../conf/nginx.conf /etc/nginx/conf.d/$domain.d/freshrss.conf
#install php5-cli
sudo apt-get update
sudo apt-get install -y php5-cli
#install update cron
echo "*/10 * * * * www-data /usr/bin/php $final_path/app/actualize_script.php >/tmp/FreshRSS.log 2>&1" > /tmp/cronfreshrss
sudo mv /tmp/cronttrss /etc/cron.d/freshrss
sudo chown root /etc/cron.d/freshrss
# Set permissions to freshrss directory
sudo chown -R www-data: $final_path
# Reload Nginx and regenerate SSOwat conf
sudo service nginx reload
sudo yunohost app ssowatconf

12
scripts/remove Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
db_user=freshrss
db_name=freshrss
root_pwd=$(sudo cat /etc/yunohost/mysql)
domain=$(sudo yunohost app setting roundcube domain)
mysql -u root -p$root_pwd -e "DROP DATABASE $db_name ; DROP USER $db_user@localhost ;"
sudo rm -rf /var/www/freshrss
sudo rm -f /etc/nginx/conf.d/$domain.d/freshrss.conf
sudo service nginx reload

0
scripts/upgrade Normal file
View file

280
sources/CHANGELOG Executable file
View file

@ -0,0 +1,280 @@
# Journal des modifications
## 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

662
sources/LICENSE Executable file
View file

@ -0,0 +1,662 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

100
sources/README.md Executable file
View file

@ -0,0 +1,100 @@
# 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.
* Site officiel : http://freshrss.org
* Démo : http://demo.freshrss.org/
* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
* Version actuelle : 0.7.3
* Date de publication 2014-07-21
* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
# Note sur les branches
**Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond :
* Utilisez [la branche master](https://github.com/marienfressinaud/FreshRSS/tree/master/) si vous visez la stabilité.
* [La branche beta](https://github.com/marienfressinaud/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois.
* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/marienfressinaud/FreshRSS/tree/dev) vous ouvre les bras !
# Disclaimer
Cette application a été développée pour 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)
# Pré-requis
* Serveur modeste, par exemple sous Linux ou Windows
* Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées)
* Serveur Web Apache2 ou Nginx (non testé sur les autres)
* PHP 5.2.1+ (PHP 5.3.7+ recommandé)
* Requis : [PDO_MySQL](http://php.net/pdo-mysql), [cURL](http://php.net/curl), [LibXML](http://php.net/xml), [PCRE](http://php.net/pcre), [ctype](http://php.net/ctype)
* Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv), [Zip](http://php.net/zip)
* MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+ (en bêta)
* Un navigateur Web récent tel Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
* Fonctionne aussi sur mobile
![Capture décran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.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.
# 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 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.
# 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 :
```
7 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
```
# Conseils
* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
* Le fichier `./constants.php` définit les chemins 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` et éventuellement `./data/persona/`
* Vous pouvez exporter votre liste de flux depuis FreshRSS au format OPML
* Pour sauvegarder les articles eux-même, vous pouvez utiliser [phpMyAdmin](http://www.phpmyadmin.net) ou les outils de MySQL :
```bash
mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
```
# Bibliothèques incluses
* [SimplePie](http://simplepie.org/)
* [MINZ](https://github.com/marienfressinaud/MINZ)
* [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
* [jQuery](http://jquery.com/)
* [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
* [flotr2](http://www.humblesoftware.com/flotr2)
## Uniquement pour certaines options
* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
* [phpQuery](http://code.google.com/p/phpquery/)
* [Lazy Load](http://www.appelsiini.net/projects/lazyload)
## 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)

3
sources/app/.htaccess Executable file
View file

@ -0,0 +1,3 @@
Order Allow,Deny
Deny from all
Satisfy all

View file

@ -0,0 +1,382 @@
<?php
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 = FreshRSS_Factory::createFeedDao();
$catDAO = new FreshRSS_CategoryDAO();
$defaultCategory = $catDAO->getDefault();
$defaultId = $defaultCategory->id();
if (Minz_Request::isPost()) {
$cats = Minz_Request::param('categories', array());
$ids = Minz_Request::param('ids', array());
$newCat = trim(Minz_Request::param('new_category', ''));
foreach ($cats as $key => $name) {
if (strlen($name) > 0) {
$cat = new FreshRSS_Category($name);
$values = array(
'name' => $cat->name(),
);
$catDAO->updateCategory($ids[$key], $values);
} elseif ($ids[$key] != $defaultId) {
$feedDAO->changeCategory($ids[$key], $defaultId);
$catDAO->deleteCategory($ids[$key]);
}
}
if ($newCat != '') {
$cat = new FreshRSS_Category($newCat);
$values = array(
'id' => $cat->id(),
'name' => $cat->name(),
);
if ($catDAO->searchByName($newCat) == null) {
$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();
Minz_View::prependTitle(Minz_Translate::t('categories_management') . ' · ');
}
public function feedAction() {
$catDAO = new FreshRSS_CategoryDAO();
$this->view->categories = $catDAO->listCategories(false);
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feeds = $feedDAO->listFeeds();
$id = Minz_Request::param('id');
if ($id == false && !empty($this->view->feeds)) {
$id = current($this->view->feeds)->id();
}
$this->view->flux = false;
if ($id != false) {
$this->view->flux = $this->view->feeds[$id];
if (!$this->view->flux) {
Minz_Error::error(
404,
array('error' => array(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)),
'ttl' => intval(Minz_Request::param('ttl', -2)),
);
if ($feedDAO->updateFeed($id, $values)) {
$this->view->flux->_category($cat);
$this->view->flux->faviconPrepare();
$notif = array(
'type' => 'good',
'content' => 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') . ' · ');
}
}
public function displayAction() {
if (Minz_Request::isPost()) {
$this->view->conf->_language(Minz_Request::param('language', 'en'));
$themeId = Minz_Request::param('theme', '');
if ($themeId == '') {
$themeId = FreshRSS_Themes::defaultTheme;
}
$this->view->conf->_theme($themeId);
$this->view->conf->_content_width(Minz_Request::param('content_width', 'thin'));
$this->view->conf->_topline_read(Minz_Request::param('topline_read', false));
$this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false));
$this->view->conf->_topline_date(Minz_Request::param('topline_date', false));
$this->view->conf->_topline_link(Minz_Request::param('topline_link', false));
$this->view->conf->_bottomline_read(Minz_Request::param('bottomline_read', false));
$this->view->conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false));
$this->view->conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false));
$this->view->conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false));
$this->view->conf->_bottomline_date(Minz_Request::param('bottomline_date', false));
$this->view->conf->_bottomline_link(Minz_Request::param('bottomline_link', false));
$this->view->conf->save();
Minz_Session::_param('language', $this->view->conf->language);
Minz_Translate::reset();
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);
}
$this->view->themes = FreshRSS_Themes::get();
Minz_View::prependTitle(Minz_Translate::t('display_configuration') . ' · ');
}
public function readingAction() {
if (Minz_Request::isPost()) {
$this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10));
$this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal'));
$this->view->conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL));
$this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false));
$this->view->conf->_display_posts(Minz_Request::param('display_posts', false));
$this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
$this->view->conf->_lazyload(Minz_Request::param('lazyload', false));
$this->view->conf->_sticky_post(Minz_Request::param('sticky_post', false));
$this->view->conf->_reading_confirm(Minz_Request::param('reading_confirm', false));
$this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC'));
$this->view->conf->_mark_when(array(
'article' => Minz_Request::param('mark_open_article', false),
'site' => Minz_Request::param('mark_open_site', false),
'scroll' => Minz_Request::param('mark_scroll', false),
'reception' => Minz_Request::param('mark_upon_reception', false),
));
$this->view->conf->save();
Minz_Session::_param('language', $this->view->conf->language);
Minz_Translate::reset();
invalidateHttpCache();
$notif = array(
'type' => 'good',
'content' => Minz_Translate::t('configuration_updated')
);
Minz_Session::_param('notification', $notif);
Minz_Request::forward(array('c' => 'configure', 'a' => 'reading'), true);
}
Minz_View::prependTitle(Minz_Translate::t('reading_configuration') . ' · ');
}
public function sharingAction() {
if (Minz_Request::isPost()) {
$params = Minz_Request::params();
$this->view->conf->_sharing($params['share']);
$this->view->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_View::prependTitle(Minz_Translate::t('sharing') . ' · ');
}
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->view->list_keys = $list_keys;
if (Minz_Request::isPost()) {
$shortcuts = Minz_Request::param('shortcuts');
$shortcuts_ok = array();
foreach ($shortcuts as $key => $value) {
if (in_array($value, $list_keys)) {
$shortcuts_ok[$key] = $value;
}
}
$this->view->conf->_shortcuts($shortcuts_ok);
$this->view->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_View::prependTitle(Minz_Translate::t('shortcuts') . ' · ');
}
public function usersAction() {
Minz_View::prependTitle(Minz_Translate::t('users') . ' · ');
}
public function archivingAction() {
if (Minz_Request::isPost()) {
$old = Minz_Request::param('old_entries', 3);
$keepHistoryDefault = Minz_Request::param('keep_history_default', 0);
$ttlDefault = Minz_Request::param('ttl_default', -2);
$this->view->conf->_old_entries($old);
$this->view->conf->_keep_history_default($keepHistoryDefault);
$this->view->conf->_ttl_default($ttlDefault);
$this->view->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_View::prependTitle(Minz_Translate::t('archiving_configuration') . ' · ');
$entryDAO = FreshRSS_Factory::createEntryDao();
$this->view->nb_total = $entryDAO->count();
$this->view->size_user = $entryDAO->size();
if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
$this->view->size_total = $entryDAO->size(true);
}
}
public function queriesAction() {
if (Minz_Request::isPost()) {
$queries = Minz_Request::param('queries', array());
foreach ($queries as $key => $query) {
if (!$query['name']) {
$query['name'] = Minz_Translate::t('query_number', $key + 1);
}
}
$this->view->conf->_queries($queries);
$this->view->conf->save();
$notif = array(
'type' => 'good',
'content' => Minz_Translate::t('configuration_updated')
);
Minz_Session::_param('notification', $notif);
Minz_Request::forward(array('c' => 'configure', 'a' => 'queries'), true);
} else {
$this->view->query_get = array();
foreach ($this->view->conf->queries as $key => $query) {
if (!isset($query['get'])) {
continue;
}
switch ($query['get'][0]) {
case 'c':
$dao = new FreshRSS_CategoryDAO();
$category = $dao->searchById(substr($query['get'], 2));
$this->view->query_get[$key] = array(
'type' => 'category',
'name' => $category->name(),
);
break;
case 'f':
$dao = FreshRSS_Factory::createFeedDao();
$feed = $dao->searchById(substr($query['get'], 2));
$this->view->query_get[$key] = array(
'type' => 'feed',
'name' => $feed->name(),
);
break;
case 's':
$this->view->query_get[$key] = array(
'type' => 'favorite',
'name' => 'favorite',
);
break;
case 'a':
$this->view->query_get[$key] = array(
'type' => 'all',
'name' => 'all',
);
break;
}
}
}
Minz_View::prependTitle(Minz_Translate::t('queries') . ' · ');
}
public function addQueryAction() {
$queries = $this->view->conf->queries;
$query = Minz_Request::params();
$query['name'] = Minz_Translate::t('query_number', count($queries) + 1);
unset($query['output']);
unset($query['token']);
$queries[] = $query;
$this->view->conf->_queries($queries);
$this->view->conf->save();
// Minz_Request::forward(array('params' => $query), true);
Minz_Request::forward(array('c' => 'configure', 'a' => 'queries'), true);
}
}

View file

@ -0,0 +1,163 @@
<?php
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->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);
}
}
public function lastAction () {
$ajax = Minz_Request::param ('ajax');
if (!$ajax && $this->redirect) {
Minz_Request::forward (array (
'c' => 'index',
'a' => 'index',
'params' => $this->params
), true);
} else {
Minz_Request::_param ('ajax');
}
}
public function readAction () {
$this->redirect = true;
$id = Minz_Request::param ('id');
$get = Minz_Request::param ('get');
$nextGet = Minz_Request::param ('nextGet', $get);
$idMax = Minz_Request::param ('idMax', 0);
$entryDAO = FreshRSS_Factory::createEntryDao();
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;
}
}
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feeds_marked_read')
);
Minz_Session::_param ('notification', $notif);
} else {
$is_read = (bool)(Minz_Request::param ('is_read', true));
$entryDAO->markRead ($id, $is_read);
}
}
public function bookmarkAction () {
$this->redirect = true;
$id = Minz_Request::param ('id');
if ($id) {
$entryDAO = FreshRSS_Factory::createEntryDao();
$entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true)));
}
}
public function optimizeAction() {
if (Minz_Request::isPost()) {
@set_time_limit(300);
// La table des entrées a tendance à grossir énormément
// Cette action permet d'optimiser cette table permettant de grapiller un peu de place
// Cette fonctionnalité n'est à appeler qu'occasionnellement
$entryDAO = FreshRSS_Factory::createEntryDao();
$entryDAO->optimizeTable();
$feedDAO = FreshRSS_Factory::createFeedDao();
$feedDAO->updateCachedValues();
invalidateHttpCache();
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('optimization_complete')
);
Minz_Session::_param ('notification', $notif);
}
Minz_Request::forward(array(
'c' => 'configure',
'a' => 'archiving'
), true);
}
public function purgeAction() {
@set_time_limit(300);
$nb_month_old = max($this->view->conf->old_entries, 1);
$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
$feedDAO = FreshRSS_Factory::createFeedDao();
$feeds = $feedDAO->listFeeds();
$nbTotal = 0;
invalidateHttpCache();
foreach ($feeds as $feed) {
$feedHistory = $feed->keepHistory();
if ($feedHistory == -2) { //default
$feedHistory = $this->view->conf->keep_history_default;
}
if ($feedHistory >= 0) {
$nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feedHistory);
if ($nb > 0) {
$nbTotal += $nb;
Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG);
//$feedDAO->updateLastUpdate($feed->id());
}
}
}
$feedDAO->updateCachedValues();
invalidateHttpCache();
$notif = array(
'type' => 'good',
'content' => Minz_Translate::t('purge_completed', $nbTotal)
);
Minz_Session::_param('notification', $notif);
Minz_Request::forward(array(
'c' => 'configure',
'a' => 'archiving'
), true);
}
}

View file

@ -0,0 +1,26 @@
<?php
class FreshRSS_error_Controller extends Minz_ActionController {
public function indexAction () {
switch (Minz_Request::param ('code')) {
case 403:
$this->view->code = 'Error 403 - Forbidden';
break;
case 404:
$this->view->code = 'Error 404 - Not found';
break;
case 500:
$this->view->code = 'Error 500 - Internal Server Error';
break;
case 503:
$this->view->code = 'Error 503 - Service Unavailable';
break;
default:
$this->view->code = 'Error 404 - Not found';
}
$this->view->logs = Minz_Request::param ('logs');
Minz_View::prependTitle ($this->view->code . ' · ');
}
}

View file

@ -0,0 +1,422 @@
<?php
class FreshRSS_feed_Controller extends Minz_ActionController {
public function firstAction () {
if (!$this->view->loginOk) {
// 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_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')))
);
}
}
}
public function addAction () {
$url = Minz_Request::param('url_rss', false);
if ($url === false) {
Minz_Request::forward(array(
'c' => 'configure',
'a' => 'feed'
), true);
}
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->catDAO = new FreshRSS_CategoryDAO ();
$this->catDAO->checkDefault ();
if (Minz_Request::isPost()) {
@set_time_limit(300);
$cat = Minz_Request::param ('category', false);
if ($cat === 'nc') {
$new_cat = Minz_Request::param ('new_category');
if (empty($new_cat['name'])) {
$cat = false;
} else {
$cat = $this->catDAO->addCategory($new_cat);
}
}
if ($cat === false) {
$def_cat = $this->catDAO->getDefault ();
$cat = $def_cat->id ();
}
$user = Minz_Request::param ('http_user');
$pass = Minz_Request::param ('http_pass');
$params = array ();
$transactionStarted = false;
try {
$feed = new FreshRSS_Feed ($url);
$feed->_category ($cat);
$httpAuth = '';
if ($user != '' || $pass != '') {
$httpAuth = $user . ':' . $pass;
}
$feed->_httpAuth ($httpAuth);
$feed->load(true);
$values = array (
'url' => $feed->url (),
'category' => $feed->category (),
'name' => $feed->name (),
'website' => $feed->website (),
'description' => $feed->description (),
'lastUpdate' => time (),
'httpAuth' => $feed->httpAuth (),
);
if ($feedDAO->searchByUrl ($values['url'])) {
// on est déjà abonné à ce flux
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('already_subscribed', $feed->name ())
);
Minz_Session::_param ('notification', $notif);
} else {
$id = $feedDAO->addFeed ($values);
if (!$id) {
// problème au niveau de la base de données
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('feed_not_added', $feed->name ())
);
Minz_Session::_param ('notification', $notif);
} else {
$feed->_id ($id);
$feed->faviconPrepare();
$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
$entryDAO = FreshRSS_Factory::createEntryDao();
$entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order
// on calcule la date des articles les plus anciens qu'on accepte
$nb_month_old = $this->view->conf->old_entries;
$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
//MySQL: http://docs.oracle.com/cd/E17952_01/refman-5.5-en/optimizing-innodb-transaction-management.html
//SQLite: http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite
$preparedStatement = $entryDAO->addEntryPrepare();
$transactionStarted = true;
$feedDAO->beginTransaction();
// on ajoute les articles en masse sans vérification
foreach ($entries as $entry) {
$values = $entry->toArray();
$values['id_feed'] = $feed->id();
$values['id'] = min(time(), $entry->date(true)) . uSecString();
$values['is_read'] = $is_read;
$entryDAO->addEntry($values, $preparedStatement);
}
$feedDAO->updateLastUpdate($feed->id());
if ($transactionStarted) {
$feedDAO->commit();
}
$transactionStarted = false;
// ok, ajout terminé
$notif = array (
'type' => 'good',
'content' => Minz_Translate::t ('feed_added', $feed->name ())
);
Minz_Session::_param ('notification', $notif);
// permet de rediriger vers la page de conf du flux
$params['id'] = $feed->id ();
}
}
} 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);
} catch (FreshRSS_Feed_Exception $e) {
Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs')))
);
Minz_Session::_param ('notification', $notif);
} catch (Minz_FileNotExistException $e) {
// Répertoire de cache n'existe pas
Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
$notif = array (
'type' => 'bad',
'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs')))
);
Minz_Session::_param ('notification', $notif);
}
if ($transactionStarted) {
$feedDAO->rollBack ();
}
Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
} else {
// GET request so we must ask confirmation to user
Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · ');
$this->view->categories = $this->catDAO->listCategories();
$this->view->feed = new FreshRSS_Feed($url);
try {
// We try to get some 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
$notif = array(
'type' => 'bad',
'content' => Minz_Translate::t(
'already_subscribed', $feed->name()
)
);
Minz_Session::_param('notification', $notif);
Minz_Request::forward(array(
'c' => 'configure',
'a' => 'feed',
'params' => array(
'id' => $feed->id()
)
), true);
}
}
}
public function truncateAction () {
if (Minz_Request::isPost ()) {
$id = Minz_Request::param ('id');
$feedDAO = FreshRSS_Factory::createFeedDao();
$n = $feedDAO->truncate($id);
$notif = array(
'type' => $n === false ? 'bad' : 'good',
'content' => Minz_Translate::t ('n_entries_deleted', $n)
);
Minz_Session::_param ('notification', $notif);
invalidateHttpCache();
Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true);
}
}
public function actualizeAction () {
@set_time_limit(300);
$feedDAO = FreshRSS_Factory::createFeedDao();
$entryDAO = FreshRSS_Factory::createEntryDao();
Minz_Session::_param('actualize_feeds', false);
$id = Minz_Request::param ('id');
$force = Minz_Request::param ('force', false);
// 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);
if ($feed) {
$feeds = array ($feed);
}
} else {
$feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
}
// on calcule la date des articles les plus anciens qu'on accepte
$nb_month_old = max($this->view->conf->old_entries, 1);
$date_min = time () - (3600 * 24 * 30 * $nb_month_old);
$i = 0;
$flux_update = 0;
$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
foreach ($feeds as $feed) {
if (!$feed->lock()) {
Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE);
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;
}
$preparedStatement = $entryDAO->addEntryPrepare();
$hasTransaction = true;
$feedDAO->beginTransaction();
// On ne vérifie pas strictement que l'article n'est pas déjà en BDD
// La BDD refusera l'ajout car (id_feed, guid) doit être unique
foreach ($entries as $entry) {
$eDate = $entry->date(true);
if ((!isset($existingGuids[$entry->guid()])) &&
(($feedHistory != 0) || ($eDate >= $date_min))) {
$values = $entry->toArray();
//Use declared date at first import, otherwise use discovery date
$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
min(time(), $eDate) . uSecString() :
uTimeString();
$values['is_read'] = $is_read;
$entryDAO->addEntry($values, $preparedStatement);
}
}
}
if (($feedHistory >= 0) && (rand(0, 30) === 1)) {
if (!$hasTransaction) {
$feedDAO->beginTransaction();
}
$nb = $feedDAO->cleanOldEntries ($feed->id (), $date_min, max($feedHistory, count($entries) + 10));
if ($nb > 0) {
Minz_Log::record ($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG);
}
}
// on indique que le flux vient d'être mis à jour en BDD
$feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction);
if ($hasTransaction) {
$feedDAO->commit();
}
$flux_update++;
if (($feed->url() !== $url)) { //HTTP 301 Moved Permanently
Minz_Log::record('Feed ' . $url . ' moved permanently to ' . $feed->url(), Minz_Log::NOTICE);
$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
}
} catch (FreshRSS_Feed_Exception $e) {
Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
$feedDAO->updateLastUpdate ($feed->id (), 1);
}
$feed->faviconPrepare();
$feed->unlock();
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) {
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);
}
}
public function deleteAction () {
if (Minz_Request::isPost ()) {
$type = Minz_Request::param ('type', 'feed');
$id = Minz_Request::param ('id');
$feedDAO = FreshRSS_Factory::createFeedDao();
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')
);
}
} 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_Session::_param ('notification', $notif);
if ($type == 'category') {
Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
} else {
Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed'), true);
}
}
}
}

View file

@ -0,0 +1,391 @@
<?php
class FreshRSS_importExport_Controller extends Minz_ActionController {
public function firstAction() {
if (!$this->view->loginOk) {
Minz_Error::error(
403,
array('error' => array(Minz_Translate::t('access_denied')))
);
}
require_once(LIB_PATH . '/lib_opml.php');
$this->catDAO = new FreshRSS_CategoryDAO();
$this->entryDAO = FreshRSS_Factory::createEntryDao();
$this->feedDAO = FreshRSS_Factory::createFeedDao();
}
public function indexAction() {
$this->view->categories = $this->catDAO->listCategories();
$this->view->feeds = $this->feedDAO->listFeeds();
Minz_View::prependTitle(Minz_Translate::t('import_export') . ' · ');
}
public function importAction() {
if (Minz_Request::isPost() && $_FILES['file']['error'] == 0) {
@set_time_limit(300);
$file = $_FILES['file'];
$type_file = $this->guessFileType($file['name']);
$list_files = array(
'opml' => array(),
'json_starred' => array(),
'json_feed' => array()
);
// We try to list all files according to their type
// A zip file is first opened and then its files are listed
$list = array();
if ($type_file === 'zip') {
$zip = zip_open($file['tmp_name']);
while (($zipfile = zip_read($zip)) !== false) {
$type_zipfile = $this->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 !== 'unknown') {
$list_files[$type_file][] = file_get_contents(
$file['tmp_name']
);
}
// Import different files.
// OPML first(so categories and feeds are imported)
// Starred articles then so the "favourite" status is already set
// And finally all other files.
$error = false;
foreach ($list_files['opml'] as $opml_file) {
$error = $this->importOpml($opml_file);
}
foreach ($list_files['json_starred'] as $article_file) {
$error = $this->importArticles($article_file, true);
}
foreach ($list_files['json_feed'] as $article_file) {
$error = $this->importArticles($article_file);
}
// And finally, we get import status and redirect to the home page
$notif = null;
if ($error === true) {
$content_notif = Minz_Translate::t(
'feeds_imported_with_errors'
);
} else {
$content_notif = Minz_Translate::t(
'feeds_imported'
);
}
Minz_Session::_param('notification', array(
'type' => 'good',
'content' => $content_notif
));
Minz_Session::_param('actualize_feeds', true);
Minz_Request::forward(array(
'c' => 'index',
'a' => 'index'
), true);
}
// What are you doing? you have to call this controller
// with a POST request!
Minz_Request::forward(array(
'c' => 'importExport',
'a' => 'index'
));
}
private function guessFileType($filename) {
// A *very* basic guess file type function. Only based on filename
// That's could be improved but should be enough, at least for a first
// implementation.
// TODO: improve this function?
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 (strcmp($filename, 'starred.json') === 0) {
return 'json_starred';
} elseif (substr_compare($filename, '.json', -5) === 0 &&
strpos($filename, 'feed_') === 0) {
return 'json_feed';
} else {
return 'unknown';
}
}
private function importOpml($opml_file) {
$opml_array = array();
try {
$opml_array = libopml_parse_string($opml_file);
} catch (LibOPML_Exception $e) {
Minz_Log::warning($e->getMessage());
return true;
}
$this->catDAO->checkDefault();
return $this->addOpmlElements($opml_array['body']);
}
private function addOpmlElements($opml_elements, $parent_cat = null) {
$error = false;
foreach ($opml_elements as $elt) {
$res = false;
if (isset($elt['xmlUrl'])) {
$res = $this->addFeedOpml($elt, $parent_cat);
} else {
$res = $this->addCategoryOpml($elt, $parent_cat);
}
if (!$error && $res) {
// oops: there is at least one error!
$error = $res;
}
}
return $error;
}
private function addFeedOpml($feed_elt, $parent_cat) {
if (is_null($parent_cat)) {
// This feed has no parent category so we get the default one
$parent_cat = $this->catDAO->getDefault()->name();
}
$cat = $this->catDAO->searchByName($parent_cat);
if (!$cat) {
return true;
}
// We get different useful information
$url = html_chars_utf8($feed_elt['xmlUrl']);
$name = html_chars_utf8($feed_elt['text']);
$website = '';
if (isset($feed_elt['htmlUrl'])) {
$website = html_chars_utf8($feed_elt['htmlUrl']);
}
$description = '';
if (isset($feed_elt['description'])) {
$description = html_chars_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);
// addFeedObject checks if feed is already in DB so nothing else to
// check here
$id = $this->feedDAO->addFeedObject($feed);
$error = ($id === false);
} catch (FreshRSS_Feed_Exception $e) {
Minz_Log::warning($e->getMessage());
$error = true;
}
return $error;
}
private function addCategoryOpml($cat_elt, $parent_cat) {
// Create a new Category object
$cat = new FreshRSS_Category(html_chars_utf8($cat_elt['text']));
$id = $this->catDAO->addCategoryObject($cat);
$error = ($id === false);
if (isset($cat_elt['@outlines'])) {
// Our cat_elt contains more categories or more feeds, so we
// add them recursively.
// Note: FreshRSS does not support yet category arborescence
$res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name());
if (!$error && $res) {
$error = true;
}
}
return $error;
}
private function importArticles($article_file, $starred = false) {
$article_object = json_decode($article_file, true);
if (is_null($article_object)) {
Minz_Log::warning('Try to import a non-JSON file');
return true;
}
$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
$google_compliant = (
strpos($article_object['id'], 'com.google') !== false
);
$error = false;
foreach ($article_object['items'] as $item) {
$feed = $this->addFeedArticles($item['origin'], $google_compliant);
if (is_null($feed)) {
$error = true;
continue;
}
$author = isset($item['author']) ? $item['author'] : '';
$key_content = ($google_compliant && !isset($item['content'])) ?
'summary' : 'content';
$tags = $item['categories'];
if ($google_compliant) {
$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->_tags($tags);
//FIME: Use entryDAO->addEntryPrepare(). Do not call entryDAO->listLastGuidsByFeed() for each entry. Consider using a transaction.
$id = $this->entryDAO->addEntryObject(
$entry, $this->view->conf, $feed->keepHistory()
);
if (!$error && ($id === false)) {
$error = true;
}
}
return $error;
}
private function addFeedArticles($origin, $google_compliant) {
$default_cat = $this->catDAO->getDefault();
$return = null;
$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
$url = $origin[$key];
$name = $origin['title'];
$website = $origin['htmlUrl'];
$error = false;
try {
// Create a Feed object and add it in DB
$feed = new FreshRSS_Feed($url);
$feed->_category($default_cat->id());
$feed->_name($name);
$feed->_website($website);
// addFeedObject checks if feed is already in DB so nothing else to
// check here
$id = $this->feedDAO->addFeedObject($feed);
if ($id !== false) {
$feed->_id($id);
$return = $feed;
}
} catch (FreshRSS_Feed_Exception $e) {
Minz_Log::warning($e->getMessage());
}
return $return;
}
public function exportAction() {
if (Minz_Request::isPost()) {
$this->view->_useLayout(false);
$export_opml = Minz_Request::param('export_opml', false);
$export_starred = Minz_Request::param('export_starred', false);
$export_feeds = Minz_Request::param('export_feeds', false);
// From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
$file = tempnam('tmp', 'zip');
$zip = new ZipArchive();
$zip->open($file, ZipArchive::OVERWRITE);
// Stuff with content
if ($export_opml) {
$zip->addFromString(
'feeds.opml', $this->generateOpml()
);
}
if ($export_starred) {
$zip->addFromString(
'starred.json', $this->generateArticles('starred')
);
}
foreach ($export_feeds as $feed_id) {
$feed = $this->feedDAO->searchById($feed_id);
$zip->addFromString(
'feed_' . $feed->category() . '_' . $feed->id() . '.json',
$this->generateArticles('feed', $feed)
);
}
// Close and send to user
$zip->close();
header('Content-Type: application/zip');
header('Content-Length: ' . filesize($file));
header('Content-Disposition: attachment; filename="freshrss_export.zip"');
readfile($file);
unlink($file);
}
}
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');
}
private function generateArticles($type, $feed = NULL) {
$this->view->categories = $this->catDAO->listCategories();
if ($type == 'starred') {
$this->view->list_title = Minz_Translate::t('starred_list');
$this->view->type = 'starred';
$unread_fav = $this->entryDAO->countUnreadReadFavorites();
$this->view->entries = $this->entryDAO->listWhere(
's', '', FreshRSS_Entry::STATE_ALL, 'ASC',
$unread_fav['all']
);
} elseif ($type == 'feed' && !is_null($feed)) {
$this->view->list_title = Minz_Translate::t(
'feed_list', $feed->name()
);
$this->view->type = 'feed/' . $feed->id();
$this->view->entries = $this->entryDAO->listWhere(
'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
$this->view->conf->posts_per_page
);
$this->view->feed = $feed;
}
return $this->view->helperToString('export/articles');
}
}

View file

@ -0,0 +1,379 @@
<?php
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;
}
}
$params = Minz_Request::params ();
if (isset ($params['search'])) {
$params['search'] = urlencode ($params['search']);
}
$this->view->url = array (
'c' => 'index',
'a' => 'index',
'params' => $params
);
if ($output === 'rss') {
// no layout for RSS output
$this->view->_useLayout (false);
header('Content-Type: application/rss+xml; charset=utf-8');
} elseif ($output === 'global') {
Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
}
$catDAO = new FreshRSS_CategoryDAO();
$entryDAO = FreshRSS_Factory::createEntryDao();
$this->view->cat_aside = $catDAO->listCategories ();
$this->view->nb_favorites = $entryDAO->countUnreadReadFavorites ();
$this->view->nb_not_read = FreshRSS_CategoryDAO::CountUnreads($this->view->cat_aside, 1);
$this->view->currentName = '';
$this->view->get_c = '';
$this->view->get_f = '';
$get = Minz_Request::param ('get', 'a');
$getType = $get[0];
$getId = substr ($get, 2);
if (!$this->checkAndProcessType ($getType, $getId)) {
Minz_Log::record ('Not found [' . $getType . '][' . $getId . ']', Minz_Log::DEBUG);
Minz_Error::error (
404,
array ('error' => array (Minz_Translate::t ('page_not_found')))
);
return;
}
// mise à jour des titres
$this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
if ($this->view->nb_not_read > 0) {
Minz_View::prependTitle('(' . formatNumber($this->view->nb_not_read) . ') ');
}
Minz_View::prependTitle(
($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') .
$this->view->currentName .
' · '
);
// On récupère les différents éléments de filtrage
$this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view);
$state_param = Minz_Request::param ('state', null);
$filter = Minz_Request::param ('search', '');
$this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order);
$nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page);
$first = Minz_Request::param ('next', '');
if ($state === FreshRSS_Entry::STATE_NOT_READ) { //Any unread article in this category at all?
switch ($getType) {
case 'a':
$hasUnread = $this->view->nb_not_read > 0;
break;
case 's':
// This is deprecated. The favorite button does not exist anymore
$hasUnread = $this->view->nb_favorites['unread'] > 0;
break;
case 'c':
$hasUnread = (!isset($this->view->cat_aside[$getId])) || ($this->view->cat_aside[$getId]->nbNotRead() > 0);
break;
case 'f':
$myFeed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
$hasUnread = ($myFeed === null) || ($myFeed->nbNotRead() > 0);
break;
default:
$hasUnread = true;
break;
}
if (!$hasUnread && ($state_param === null)) {
$this->view->state = $state = FreshRSS_Entry::STATE_ALL;
}
}
$today = @strtotime('today');
$this->view->today = $today;
// on calcule la date des articles les plus anciens qu'on affiche
$nb_month_old = $this->view->conf->old_entries;
$date_min = $today - (3600 * 24 * 30 * $nb_month_old); //Do not use a fast changing value such as time() to allow SQL caching
$keepHistoryDefault = $this->view->conf->keep_history_default;
try {
$entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault);
// Si on a récupéré aucun article "non lus"
// on essaye de récupérer tous les articles
if ($state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
Minz_Log::record('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
$feedDAO = FreshRSS_Factory::createFeedDao();
try {
$feedDAO->updateCachedValues();
} catch (Exception $ex) {
Minz_Log::record('Failed to automatically correct nbNotRead! ' + $ex->getMessage(), Minz_Log::NOTICE);
}
$this->view->state = FreshRSS_Entry::STATE_ALL;
$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault);
}
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')))
);
}
}
/*
* 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
*/
private function checkAndProcessType ($getType, $getId) {
switch ($getType) {
case 'a':
$this->view->currentName = Minz_Translate::t ('your_rss_feeds');
$this->nb_not_read_cat = $this->view->nb_not_read;
$this->view->get_c = $getType;
return true;
case 's':
$this->view->currentName = Minz_Translate::t ('your_favorites');
$this->nb_not_read_cat = $this->view->nb_favorites['unread'];
$this->view->get_c = $getType;
return true;
case 'c':
$cat = isset($this->view->cat_aside[$getId]) ? $this->view->cat_aside[$getId] : null;
if ($cat === null) {
$catDAO = new FreshRSS_CategoryDAO();
$cat = $catDAO->searchById($getId);
}
if ($cat) {
$this->view->currentName = $cat->name ();
$this->nb_not_read_cat = $cat->nbNotRead ();
$this->view->get_c = $getId;
return true;
} else {
return false;
}
case 'f':
$feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
if (empty($feed)) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($getId);
}
if ($feed) {
$this->view->currentName = $feed->name ();
$this->nb_not_read_cat = $feed->nbNotRead ();
$this->view->get_f = $getId;
$this->view->get_c = $feed->category ();
return true;
} else {
return false;
}
default:
return false;
}
}
public function aboutAction () {
Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
}
public function logsAction () {
if (!$this->view->loginOk) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
}
Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · ');
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::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) {
Minz_Session::_param('currentUser');
Minz_Session::_param('mail');
Minz_Session::_param('passwordHash');
$username = ctype_alnum($_GET['u']) ? $_GET['u'] : '';
$passwordPlain = $_GET['p'];
Minz_Request::_param('p'); //Discard plain-text password ASAP
$_GET['p'] = '';
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
try {
$conf = new FreshRSS_Configuration($username);
$s = $conf->passwordHash;
$ok = password_verify($passwordPlain, $s);
unset($passwordPlain);
if ($ok) {
Minz_Session::_param('currentUser', $username);
Minz_Session::_param('passwordHash', $s);
} else {
Minz_Log::record('Unsafe password mismatch for user ' . $username, Minz_Log::WARNING);
}
} catch (Minz_Exception $me) {
Minz_Log::record('Unsafe login failure: ' . $me->getMessage(), Minz_Log::WARNING);
}
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
} elseif (!Minz_Configuration::canLogIn()) {
Minz_Error::error (
403,
array ('error' => array (Minz_Translate::t ('access_denied')))
);
}
invalidateHttpCache();
}
public function formLogoutAction () {
$this->view->_useLayout(false);
invalidateHttpCache();
Minz_Session::_param('currentUser');
Minz_Session::_param('mail');
Minz_Session::_param('passwordHash');
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
}
}

View file

@ -0,0 +1,46 @@
<?php
class FreshRSS_javascript_Controller extends Minz_ActionController {
public function firstAction () {
$this->view->_useLayout (false);
}
public function actualizeAction () {
header('Content-Type: text/javascript; charset=UTF-8');
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
}
public function nbUnreadsPerFeedAction() {
header('Content-Type: application/json; charset=UTF-8');
$catDAO = new FreshRSS_CategoryDAO();
$this->view->categories = $catDAO->listCategories(true, false);
}
//For Web-form login
public function nonceAction() {
header('Content-Type: application/json; charset=UTF-8');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
header('Expires: 0');
header('Cache-Control: private, no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
$user = isset($_GET['user']) ? $_GET['user'] : '';
if (ctype_alnum($user)) {
try {
$conf = new FreshRSS_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));
Minz_Session::_param('nonce', $this->view->nonce);
return; //Success
}
} catch (Minz_Exception $me) {
Minz_Log::record('Nonce failure: ' . $me->getMessage(), Minz_Log::WARNING);
}
}
$this->view->nonce = ''; //Failure
$this->view->salt1 = '';
}
}

View file

@ -0,0 +1,67 @@
<?php
class FreshRSS_stats_Controller extends Minz_ActionController {
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();
$this->view->count = ($statsDAO->calculateEntryCount());
$this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
$this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
$this->view->topFeed = $statsDAO->calculateTopFeed();
}
public function idleAction() {
$statsDAO = FreshRSS_Factory::createStatsDAO();
$feeds = $statsDAO->calculateFeedLastDate();
$idleFeeds = 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 < $lastWeek) {
$idleFeeds['last_week'][] = $feed['name'];
}
if ($feedDate < $lastMonth) {
$idleFeeds['last_month'][] = $feed['name'];
}
if ($feedDate < $last3Month) {
$idleFeeds['last_3_month'][] = $feed['name'];
}
if ($feedDate < $last6Month) {
$idleFeeds['last_6_month'][] = $feed['name'];
}
if ($feedDate < $lastYear) {
$idleFeeds['last_year'][] = $feed['name'];
}
}
$this->view->idleFeeds = array_reverse($idleFeeds);
}
public function firstAction() {
if (!$this->view->loginOk) {
Minz_Error::error(
403, array('error' => array(Minz_Translate::t('access_denied')))
);
}
Minz_View::prependTitle(Minz_Translate::t('stats') . ' · ');
}
}

View file

@ -0,0 +1,203 @@
<?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', '', true);
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);
$passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
if ($passwordPlain != '') {
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
$passwordPlain = '';
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
$ok &= ($passwordHash != '');
$this->view->conf->_apiPasswordHash($passwordHash);
}
if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
$this->view->conf->_mail_login(Minz_Request::param('mail_login', '', true));
}
$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');
$unsafe_autologin = Minz_Request::param('unsafe_autologin', false);
$api_enabled = Minz_Request::param('api_enabled', false);
if ($anon != Minz_Configuration::allowAnonymous() ||
$auth_type != Minz_Configuration::authType() ||
$anon_refresh != Minz_Configuration::allowAnonymousRefresh() ||
$unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() ||
$api_enabled != Minz_Configuration::apiEnabled()) {
Minz_Configuration::_authType($auth_type);
Minz_Configuration::_allowAnonymous($anon);
Minz_Configuration::_allowAnonymousRefresh($anon_refresh);
Minz_Configuration::_enableAutologin($unsafe_autologin);
Minz_Configuration::_enableApi($api_enabled);
$ok &= Minz_Configuration::writeFile();
}
}
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', '_'))) {
$db = Minz_Configuration::dataBase();
require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
$new_user_language = Minz_Request::param('new_user_language', $this->view->conf->language);
if (!in_array($new_user_language, $this->view->conf->availableLanguages())) {
$new_user_language = $this->view->conf->language;
}
$new_user_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', '', true);
$passwordHash = '';
if ($passwordPlain != '') {
Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
$_POST['new_user_passwordPlain'] = '';
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
$passwordPlain = '';
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
$ok &= ($passwordHash != '');
}
if (empty($passwordHash)) {
$passwordHash = '';
}
$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', '_'))) {
$db = Minz_Configuration::dataBase();
require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.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,6 @@
<?php
class FreshRSS_BadUrl_Exception extends FreshRSS_Feed_Exception {
public function __construct ($url) {
parent::__construct ('`' . $url . '` is not a valid URL');
}
}

View file

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

View file

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

146
sources/app/FreshRSS.php Executable file
View file

@ -0,0 +1,146 @@
<?php
class FreshRSS extends Minz_FrontController {
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;
}
if (!ctype_alnum($currentUser)) {
Minz_Session::_param('currentUser', '');
die('Invalid username [' . $currentUser . ']!');
}
try {
$this->conf = new FreshRSS_Configuration($currentUser);
Minz_View::_param ('conf', $this->conf);
Minz_Session::_param('currentUser', $currentUser);
} catch (Minz_Exception $me) {
$loginOk = false;
try {
$this->conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
Minz_View::_param('conf', $this->conf);
$notif = array(
'type' => 'bad',
'content' => 'Invalid configuration for user [' . $currentUser . ']!',
);
Minz_Session::_param ('notification', $notif);
Minz_Log::record ($notif['content'] . ' ' . $me->getMessage(), Minz_Log::WARNING);
Minz_Session::_param('currentUser', '');
} catch (Exception $e) {
die($e->getMessage());
}
}
if ($loginOk) {
switch (Minz_Configuration::authType()) {
case 'form':
$loginOk = Minz_Session::param('passwordHash') === $this->conf->passwordHash;
break;
case 'http_auth':
$loginOk = strcasecmp($currentUser, httpAuthUser()) === 0;
break;
case 'persona':
$loginOk = strcasecmp(Minz_Session::param('mail'), $this->conf->mail_login) === 0;
break;
case 'none':
$loginOk = true;
break;
default:
$loginOk = false;
break;
}
}
Minz_View::_param ('loginOk', $loginOk);
return $loginOk;
}
private function loadParamsView () {
Minz_Session::_param ('language', $this->conf->language);
Minz_Translate::init();
$output = Minz_Request::param ('output', '');
if (($output === '') || ($output !== 'normal' && $output !== 'rss' && $output !== 'reader' && $output !== 'global')) {
$output = $this->conf->view_mode;
Minz_Request::_param ('output', $output);
}
}
private function loadStylesAndScripts ($loginOk) {
$theme = FreshRSS_Themes::load($this->conf->theme);
if ($theme) {
foreach($theme['files'] as $file) {
Minz_View::appendStyle (Minz_Url::display ('/themes/' . $theme['id'] . '/' . $file . '?' . @filemtime(PUBLIC_PATH . '/themes/' . $theme['id'] . '/' . $file)));
}
}
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')));
}
private function loadNotifications () {
$notif = Minz_Session::param ('notification');
if ($notif) {
Minz_View::_param ('notification', $notif);
Minz_Session::_param ('notification');
}
}
}

73
sources/app/Models/Category.php Executable file
View file

@ -0,0 +1,73 @@
<?php
class FreshRSS_Category extends Minz_Model {
private $id = 0;
private $name;
private $nbFeed = -1;
private $nbNotRead = -1;
private $feeds = null;
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 ();
}
}
}
public function id () {
return $this->id;
}
public function name () {
return $this->name;
}
public function nbFeed () {
if ($this->nbFeed < 0) {
$catDAO = new FreshRSS_CategoryDAO ();
$this->nbFeed = $catDAO->countFeed ($this->id ());
}
return $this->nbFeed;
}
public function nbNotRead () {
if ($this->nbNotRead < 0) {
$catDAO = new FreshRSS_CategoryDAO ();
$this->nbNotRead = $catDAO->countNotRead ($this->id ());
}
return $this->nbNotRead;
}
public function feeds () {
if ($this->feeds === null) {
$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 ();
}
}
return $this->feeds;
}
public function _id ($value) {
$this->id = $value;
}
public function _name ($value) {
$this->name = $value;
}
public function _feeds ($values) {
if (!is_array ($values)) {
$values = array ($values);
}
$this->feeds = $values;
}
}

View file

@ -0,0 +1,257 @@
<?php
class FreshRSS_CategoryDAO extends Minz_ModelPdo {
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();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error addCategory: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
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);
}
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)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error updateCategory: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function deleteCategory ($id) {
$sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
if ($stm && $stm->execute ($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error deleteCategory: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function searchById ($id) {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=?';
$stm = $this->bd->prepare ($sql);
$values = array ($id);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$cat = self::daoToCategory ($res);
if (isset ($cat[0])) {
return $cat[0];
} else {
return null;
}
}
public function searchByName ($name) {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE name=?';
$stm = $this->bd->prepare ($sql);
$values = array ($name);
$stm->execute ($values);
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$cat = self::daoToCategory ($res);
if (isset ($cat[0])) {
return $cat[0];
} else {
return null;
}
}
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 ')
. 'FROM `' . $this->prefix . 'category` c '
. 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
. 'GROUP BY f.id '
. 'ORDER BY c.name, f.name';
$stm = $this->bd->prepare ($sql);
$stm->execute ();
return self::daoToCategoryPrepopulated ($stm->fetchAll (PDO::FETCH_ASSOC));
} 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));
}
}
public function getDefault () {
$sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=1';
$stm = $this->bd->prepare ($sql);
$stm->execute ();
$res = $stm->fetchAll (PDO::FETCH_ASSOC);
$cat = self::daoToCategory ($res);
if (isset ($cat[0])) {
return $cat[0];
} else {
return false;
}
}
public function checkDefault () {
$def_cat = $this->searchById (1);
if ($def_cat == null) {
$cat = new FreshRSS_Category (Minz_Translate::t ('default_category'));
$cat->_id (1);
$values = array (
'id' => $cat->id (),
'name' => $cat->name (),
);
$this->addCategory ($values);
}
}
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);
return $res[0]['count'];
}
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);
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);
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) {
return $feed;
}
}
}
return null;
}
public static function CountUnreads($categories, $minPriority = 0) {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds () as $feed) {
if ($feed->priority () >= $minPriority) {
$n += $feed->nbNotRead();
}
}
}
return $n;
}
public static function daoToCategoryPrepopulated ($listDAO) {
$list = array ();
if (!is_array ($listDAO)) {
$listDAO = array ($listDAO);
}
$previousLine = null;
$feedsDao = array();
foreach ($listDAO as $line) {
if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
// End of the current category, we add it to the $list
$cat = new FreshRSS_Category (
$previousLine['c_name'],
FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id'])
);
$cat->_id ($previousLine['c_id']);
$list[$previousLine['c_id']] = $cat;
$feedsDao = array(); //Prepare for next category
}
$previousLine = $line;
$feedsDao[] = $line;
}
// add the last category
if ($previousLine != null) {
$cat = new FreshRSS_Category (
$previousLine['c_name'],
FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id'])
);
$cat->_id ($previousLine['c_id']);
$list[$previousLine['c_id']] = $cat;
}
return $list;
}
public static function daoToCategory ($listDAO) {
$list = array ();
if (!is_array ($listDAO)) {
$listDAO = array ($listDAO);
}
foreach ($listDAO as $key => $dao) {
$cat = new FreshRSS_Category (
$dao['name']
);
$cat->_id ($dao['id']);
$list[$key] = $cat;
}
return $list;
}
}

View file

@ -0,0 +1,292 @@
<?php
class FreshRSS_Configuration {
private $filename;
private $data = array(
'language' => 'en',
'old_entries' => 3,
'keep_history_default' => 0,
'ttl_default' => 3600,
'mail_login' => '',
'token' => '',
'passwordHash' => '', //CRYPT_BLOWFISH
'apiPasswordHash' => '', //CRYPT_BLOWFISH
'posts_per_page' => 20,
'view_mode' => 'normal',
'default_view' => FreshRSS_Entry::STATE_NOT_READ,
'auto_load_more' => true,
'display_posts' => false,
'onread_jump_next' => true,
'lazyload' => true,
'sticky_post' => true,
'reading_confirm' => false,
'sort_order' => 'DESC',
'anon_access' => false,
'mark_when' => array(
'article' => true,
'site' => true,
'scroll' => false,
'reception' => false,
),
'theme' => 'Origine',
'content_width' => 'thin',
'shortcuts' => array(
'mark_read' => 'r',
'mark_favorite' => 'f',
'go_website' => 'space',
'next_entry' => 'j',
'prev_entry' => 'k',
'first_entry' => 'home',
'last_entry' => 'end',
'collapse_entry' => 'c',
'load_more' => 'm',
'auto_share' => 's',
'focus_search' => 'a',
),
'topline_read' => true,
'topline_favorite' => true,
'topline_date' => true,
'topline_link' => true,
'bottomline_read' => true,
'bottomline_favorite' => true,
'bottomline_sharing' => true,
'bottomline_tags' => true,
'bottomline_date' => true,
'bottomline_link' => true,
'sharing' => array(),
'queries' => array(),
);
private $available_languages = array(
'en' => 'English',
'fr' => 'Français',
);
private $shares;
public function __construct($user) {
$this->filename = DATA_PATH . DIRECTORY_SEPARATOR . $user . '_user.php';
$data = @include($this->filename);
if (!is_array($data)) {
throw new Minz_PermissionDeniedException($this->filename);
}
foreach ($data as $key => $value) {
if (isset($this->data[$key])) {
$function = '_' . $key;
$this->$function($value);
}
}
$this->data['user'] = $user;
$this->shares = DATA_PATH . DIRECTORY_SEPARATOR . 'shares.php';
$shares = @include($this->shares);
if (!is_array($shares)) {
throw new Minz_PermissionDeniedException($this->shares);
}
$this->data['shares'] = $shares;
}
public function save() {
@rename($this->filename, $this->filename . '.bak.php');
unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration
if (file_put_contents($this->filename, "<?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 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 === FreshRSS_Entry::STATE_ALL ? FreshRSS_Entry::STATE_ALL : FreshRSS_Entry::STATE_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 _sticky_post($value) {
$this->data['sticky_post'] = ((bool)$value) && $value !== 'no';
}
public function _reading_confirm($value) {
$this->data['reading_confirm'] = ((bool)$value) && $value !== 'no';
}
public function _sort_order ($value) {
$this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC';
}
public function _old_entries($value) {
$value = intval($value);
$this->data['old_entries'] = $value > 0 ? $value : 3;
}
public function _keep_history_default($value) {
$value = intval($value);
$this->data['keep_history_default'] = $value >= -1 ? $value : 0;
}
public function _ttl_default($value) {
$value = intval($value);
$this->data['ttl_default'] = $value >= -1 ? $value : 3600;
}
public function _shortcuts ($values) {
foreach ($values as $key => $value) {
if (isset($this->data['shortcuts'][$key])) {
$this->data['shortcuts'][$key] = $value;
}
}
}
public function _passwordHash ($value) {
$this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
public function _apiPasswordHash ($value) {
$this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
}
public function _mail_login ($value) {
$value = filter_var($value, FILTER_VALIDATE_EMAIL);
if ($value) {
$this->data['mail_login'] = $value;
} else {
$this->data['mail_login'] = '';
}
}
public function _anon_access ($value) {
$this->data['anon_access'] = ((bool)$value) && $value !== 'no';
}
public function _mark_when ($values) {
foreach ($values as $key => $value) {
if (isset($this->data['mark_when'][$key])) {
$this->data['mark_when'][$key] = ((bool)$value) && $value !== 'no';
}
}
}
public function _sharing ($values) {
$this->data['sharing'] = array();
foreach ($values as $value) {
if (!is_array($value)) {
continue;
}
// Verify URL and add default value when needed
if (isset($value['url'])) {
$is_url = (
filter_var ($value['url'], FILTER_VALIDATE_URL) ||
(version_compare(PHP_VERSION, '5.3.3', '<') &&
(strpos($value, '-') > 0) &&
($value === filter_var($value, FILTER_SANITIZE_URL)))
); //PHP bug #51192
if (!$is_url) {
continue;
}
} else {
$value['url'] = null;
}
// Add a default name
if (empty($value['name'])) {
$value['name'] = $value['type'];
}
$this->data['sharing'][] = $value;
}
}
public function _queries ($values) {
$this->data['queries'] = array();
foreach ($values as $value) {
$value = array_filter($value);
$params = $value;
unset($params['name']);
unset($params['url']);
$value['url'] = Minz_Url::display(array('params' => $params));
$this->data['queries'][] = $value;
}
}
public function _theme($value) {
$this->data['theme'] = $value;
}
public function _content_width($value) {
if ($value === 'medium' ||
$value === 'large' ||
$value === 'no_limit') {
$this->data['content_width'] = $value;
} else {
$this->data['content_width'] = 'thin';
}
}
public function _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';
}
}

7
sources/app/Models/Days.php Executable file
View file

@ -0,0 +1,7 @@
<?php
class FreshRSS_Days {
const TODAY = 0;
const YESTERDAY = 1;
const BEFORE_YESTERDAY = 2;
}

191
sources/app/Models/Entry.php Executable file
View file

@ -0,0 +1,191 @@
<?php
class FreshRSS_Entry extends Minz_Model {
const STATE_ALL = 0;
const STATE_READ = 1;
const STATE_NOT_READ = 2;
const STATE_FAVORITE = 4;
const STATE_NOT_FAVORITE = 8;
private $id = 0;
private $guid;
private $title;
private $author;
private $content;
private $link;
private $date;
private $is_read;
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 id () {
return $this->id;
}
public function guid () {
return $this->guid;
}
public function title () {
return $this->title;
}
public function author () {
return $this->author === null ? '' : $this->author;
}
public function content () {
return $this->content;
}
public function link () {
return $this->link;
}
public function date ($raw = false) {
if ($raw) {
return $this->date;
} else {
return timestamptodate ($this->date);
}
}
public function dateAdded ($raw = false) {
$date = intval(substr($this->id, 0, -6));
if ($raw) {
return $date;
} else {
return timestamptodate ($date);
}
}
public function isRead () {
return $this->is_read;
}
public function isFavorite () {
return $this->is_favorite;
}
public function feed ($object = false) {
if ($object) {
$feedDAO = FreshRSS_Factory::createFeedDao();
return $feedDAO->searchById ($this->feed);
} else {
return $this->feed;
}
}
public function tags ($inString = false) {
if ($inString) {
return empty ($this->tags) ? '' : '#' . implode(' #', $this->tags);
} else {
return $this->tags;
}
}
public function _id ($value) {
$this->id = $value;
}
public function _guid ($value) {
$this->guid = $value;
}
public function _title ($value) {
$this->title = $value;
}
public function _author ($value) {
$this->author = $value;
}
public function _content ($value) {
$this->content = $value;
}
public function _link ($value) {
$this->link = $value;
}
public function _date ($value) {
$value = intval($value);
$this->date = $value > 1 ? $value : time();
}
public function _isRead ($value) {
$this->is_read = $value;
}
public function _isFavorite ($value) {
$this->is_favorite = $value;
}
public function _feed ($value) {
$this->feed = $value;
}
public function _tags ($value) {
if (!is_array ($value)) {
$value = array ($value);
}
foreach ($value as $key => $t) {
if (!$t) {
unset ($value[$key]);
}
}
$this->tags = $value;
}
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;
}
}
public function loadCompleteContent($pathEntries) {
// Gestion du contenu
// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
if ($pathEntries) {
$entryDAO = FreshRSS_Factory::createEntryDao();
$entry = $entryDAO->searchByGuid($this->feed, $this->guid);
if($entry) {
// l'article existe déjà en BDD, en se contente de recharger ce contenu
$this->content = $entry->content();
} else {
try {
// l'article n'est pas en BDD, on va le chercher sur le site
$this->content = get_content_by_parsing(
htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries
);
} catch (Exception $e) {
// 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),
);
}
}

562
sources/app/Models/EntryDAO.php Executable file
View file

@ -0,0 +1,562 @@
<?php
class FreshRSS_EntryDAO extends Minz_ModelPdo {
public function isCompressed() {
return parent::$sharedDbType !== 'sqlite';
}
public function addEntryPrepare() {
$sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, '
. ($this->isCompressed() ? 'content_bin' : 'content')
. ', link, date, is_read, is_favorite, id_feed, tags) '
. 'VALUES(?, ?, ?, ?, '
. ($this->isCompressed() ? 'COMPRESS(?)' : '?')
. ', ?, ?, ?, ?, ?, ?)';
return $this->bd->prepare($sql);
}
public function addEntry($valuesTmp, $preparedStatement = null) {
$stm = $preparedStatement === null ? addEntryPrepare() : $preparedStatement;
$values = array(
$valuesTmp['id'],
substr($valuesTmp['guid'], 0, 760),
substr($valuesTmp['title'], 0, 255),
substr($valuesTmp['author'], 0, 255),
$valuesTmp['content'],
substr($valuesTmp['link'], 0, 1023),
$valuesTmp['date'],
$valuesTmp['is_read'] ? 1 : 0,
$valuesTmp['is_favorite'] ? 1 : 0,
$valuesTmp['id_feed'],
substr($valuesTmp['tags'], 0, 1023),
);
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
if ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
Minz_Log::record('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::ERROR);
} /*else {
Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::DEBUG);
}*/
return false;
}
}
public function addEntryObject($entry, $conf, $feedHistory) {
$existingGuids = array_fill_keys(
$this->listLastGuidsByFeed($entry->feed(), 20), 1
);
$nb_month_old = max($conf->old_entries, 1);
$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
$eDate = $entry->date(true);
if ($feedHistory == -2) {
$feedHistory = $conf->keep_history_default;
}
if (!isset($existingGuids[$entry->guid()]) &&
($feedHistory != 0 || $eDate >= $date_min)) {
$values = $entry->toArray();
$useDeclaredDate = empty($existingGuids);
$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
min(time(), $eDate) . uSecString() :
uTimeString();
return $this->addEntry($values);
}
// We don't return Entry object to avoid a research in DB
return -1;
}
public function markFavorite($ids, $is_favorite = true) {
if (!is_array($ids)) {
$ids = array($ids);
}
$sql = 'UPDATE `' . $this->prefix . 'entry` '
. 'SET is_favorite=? '
. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
$values = array($is_favorite ? 1 : 0);
$values = array_merge($values, $ids);
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markFavorite: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
protected function updateCacheUnreads($catId = false, $feedId = false) {
$sql = 'UPDATE `' . $this->prefix . 'feed` f '
. 'LEFT OUTER JOIN ('
. 'SELECT e.id_feed, '
. 'COUNT(*) AS nbUnreads '
. 'FROM `' . $this->prefix . 'entry` e '
. 'WHERE e.is_read=0 '
. 'GROUP BY e.id_feed'
. ') x ON x.id_feed=f.id '
. 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
. 'WHERE 1';
$values = array();
if ($feedId !== false) {
$sql .= ' AND f.id=?';
$values[] = $id;
}
if ($catId !== false) {
$sql .= ' AND f.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::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function markRead($ids, $is_read = true) {
if (is_array($ids)) { //Many IDs at once (used by API)
if (count($ids) < 6) { //Speed heuristics
$affected = 0;
foreach ($ids as $id) {
$affected += $this->markRead($id, $is_read);
}
return $affected;
}
$sql = 'UPDATE `' . $this->prefix . 'entry` '
. 'SET is_read=? '
. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
$values = array($is_read ? 1 : 0);
$values = array_merge($values, $ids);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR);
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
return false;
}
return $affected;
} else {
$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
. 'SET e.is_read=?,'
. 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
. 'WHERE e.id=? AND e.is_read=?';
$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
}
public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::record($nb . 'Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
}
$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
. 'SET e.is_read=1 '
. 'WHERE e.is_read=0 AND e.id <= ?';
if ($onlyFavorites) {
$sql .= ' AND e.is_favorite=1';
} elseif ($priorityMin >= 0) {
$sql .= ' AND f.priority > ' . intval($priorityMin);
}
$values = array($idMax);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR);
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
return false;
}
return $affected;
}
public function markReadCat($id, $idMax = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::record($nb . 'Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
}
$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
. 'SET e.is_read=1 '
. 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?';
$values = array($id, $idMax);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR);
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
return false;
}
return $affected;
}
public function markReadFeed($id, $idMax = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::record($nb . 'Calling markReadFeed(0) is deprecated!', Minz_Log::DEBUG);
}
$this->bd->beginTransaction();
$sql = 'UPDATE `' . $this->prefix . 'entry` '
. 'SET is_read=1 '
. 'WHERE id_feed=? AND is_read=0 AND id <= ?';
$values = array($id, $idMax);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR);
$this->bd->rollBack();
return false;
}
$affected = $stm->rowCount();
if ($affected > 0) {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET cache_nbUnreads=cache_nbUnreads-' . $affected
. ' 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::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR);
$this->bd->rollBack();
return false;
}
}
$this->bd->commit();
return $affected;
}
public function searchByGuid($feed_id, $id) {
// un guid est unique pour un flux donné
$sql = 'SELECT id, guid, title, author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
. ', link, date, is_read, is_favorite, id_feed, tags '
. 'FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid=?';
$stm = $this->bd->prepare($sql);
$values = array(
$feed_id,
$id
);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$entries = self::daoToEntry($res);
return isset($entries[0]) ? $entries[0] : null;
}
public function searchById($id) {
$sql = 'SELECT id, guid, title, author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
. ', link, date, is_read, is_favorite, id_feed, tags '
. 'FROM `' . $this->prefix . 'entry` WHERE id=?';
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$entries = self::daoToEntry($res);
return isset($entries[0]) ? $entries[0] : null;
}
protected function sqlConcat($s1, $s2) {
return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL
}
private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {
if (!$state) {
$state = FreshRSS_Entry::STATE_ALL;
}
$where = '';
$joinFeed = false;
$values = array();
switch ($type) {
case 'a':
$where .= 'f.priority > 0 ';
$joinFeed = true;
break;
case 's': //Deprecated: use $state instead
$where .= 'e1.is_favorite=1 ';
break;
case 'c':
$where .= 'f.category=? ';
$values[] = intval($id);
$joinFeed = true;
break;
case 'f':
$where .= 'e1.id_feed=? ';
$values[] = intval($id);
break;
case 'A':
$where .= '1 ';
break;
default:
throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
}
if ($state & FreshRSS_Entry::STATE_NOT_READ) {
if (!($state & FreshRSS_Entry::STATE_READ)) {
$where .= 'AND e1.is_read=0 ';
}
}
elseif ($state & FreshRSS_Entry::STATE_READ) {
$where .= 'AND e1.is_read=1 ';
}
if ($state & FreshRSS_Entry::STATE_FAVORITE) {
if (!($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
$where .= 'AND e1.is_favorite=1 ';
}
}
elseif ($state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
$where .= 'AND e1.is_favorite=0 ';
}
switch ($order) {
case 'DESC':
case 'ASC':
break;
default:
throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!');
}
if ($firstId === '' && parent::$sharedDbType === 'mysql') {
$firstId = $order === 'DESC' ? '9000000000'. '000000' : '0'; //MySQL optimization. Tested on MySQL 5.5 with 150k articles
}
if ($firstId !== '') {
$where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' ';
}
if (($date_min > 0) && ($type !== 's')) {
$where .= 'AND (e1.id >= ' . $date_min . '000000';
if ($showOlderUnreadsorFavorites) { //Lax date constraint
$where .= ' OR e1.is_read=0 OR e1.is_favorite=1 OR (f.keep_history <> 0';
if (intval($keepHistoryDefault) === 0) {
$where .= ' AND f.keep_history <> -2'; //default
}
$where .= ')';
}
$where .= ') ';
$joinFeed = true;
}
$search = '';
if ($filter !== '') {
require_once(LIB_PATH . '/lib_date.php');
$filter = trim($filter);
$filter = addcslashes($filter, '\\%_');
$terms = array_unique(explode(' ', $filter));
//sort($terms); //Put #tags first //TODO: Put the cheapest filters first
foreach ($terms as $word) {
$word = trim($word);
if (stripos($word, 'intitle:') === 0) {
$word = substr($word, strlen('intitle:'));
$search .= 'AND e1.title LIKE ? ';
$values[] = '%' . $word .'%';
} elseif (stripos($word, 'inurl:') === 0) {
$word = substr($word, strlen('inurl:'));
$search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? ';
$values[] = '%' . $word .'%';
} elseif (stripos($word, 'author:') === 0) {
$word = substr($word, strlen('author:'));
$search .= 'AND e1.author LIKE ? ';
$values[] = '%' . $word .'%';
} elseif (stripos($word, 'date:') === 0) {
$word = substr($word, strlen('date:'));
list($minDate, $maxDate) = parseDateInterval($word);
if ($minDate) {
$search .= 'AND e1.id >= ' . $minDate . '000000 ';
}
if ($maxDate) {
$search .= 'AND e1.id <= ' . $maxDate . '000000 ';
}
} elseif (stripos($word, 'pubdate:') === 0) {
$word = substr($word, strlen('pubdate:'));
list($minDate, $maxDate) = parseDateInterval($word);
if ($minDate) {
$search .= 'AND e1.date >= ' . $minDate . ' ';
}
if ($maxDate) {
$search .= 'AND e1.date <= ' . $maxDate . ' ';
}
} else {
if ($word[0] === '#' && isset($word[1])) {
$search .= 'AND e1.tags LIKE ? ';
$values[] = '%' . $word .'%';
} else {
$search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? ';
$values[] = '%' . $word .'%';
}
}
}
}
return array($values,
'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 '
. ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed=f.id ' : '')
. 'WHERE ' . $where
. $search
. 'ORDER BY e1.id ' . $order
. ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
}
public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) {
list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault);
$sql = 'SELECT e.id, e.guid, e.title, e.author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
. ', e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags '
. 'FROM `' . $this->prefix . 'entry` e '
. 'INNER JOIN ('
. $sql
. ') e2 ON e2.id=e.id '
. 'ORDER BY e.id ' . $order;
$stm = $this->bd->prepare($sql);
$stm->execute($values);
return self::daoToEntry($stm->fetchAll(PDO::FETCH_ASSOC));
}
public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { //For API
list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault);
$stm = $this->bd->prepare($sql);
$stm->execute($values);
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
}
public function listLastGuidsByFeed($id, $n) {
$sql = 'SELECT guid FROM `' . $this->prefix . 'entry` WHERE id_feed=? ORDER BY id DESC LIMIT ' . intval($n);
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
}
public function countUnreadRead() {
$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0'
. ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0 AND is_read=0';
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
$all = empty($res[0]) ? 0 : $res[0];
$unread = empty($res[1]) ? 0 : $res[1];
return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
}
public function count($minPriority = null) {
$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
if ($minPriority !== null) {
$sql = ' WHERE priority > ' . intval($minPriority);
}
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $res[0];
}
public function countNotRead($minPriority = null) {
$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE is_read=0';
if ($minPriority !== null) {
$sql = ' AND priority > ' . intval($minPriority);
}
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $res[0];
}
public function countUnreadReadFavorites() {
$sql = 'SELECT c FROM ('
. 'SELECT COUNT(id) AS c, 1 as o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 '
. 'UNION SELECT COUNT(id) AS c, 2 AS o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 AND is_read=0'
. ') u ORDER BY o';
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
$all = empty($res[0]) ? 0 : $res[0];
$unread = empty($res[1]) ? 0 : $res[1];
return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
}
public function optimizeTable() {
$sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`'; //MySQL
$stm = $this->bd->prepare($sql);
$stm->execute();
}
public function size($all = false) {
$db = Minz_Configuration::dataBase();
$sql = 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema=?'; //MySQL
$values = array($db['base']);
if (!$all) {
$sql .= ' AND table_name LIKE ?';
$values[] = $this->prefix . '%';
}
$stm = $this->bd->prepare($sql);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $res[0];
}
public static function daoToEntry($listDAO) {
$list = array();
if (!is_array($listDAO)) {
$listDAO = array($listDAO);
}
foreach ($listDAO as $key => $dao) {
$entry = new FreshRSS_Entry(
$dao['id_feed'],
$dao['guid'],
$dao['title'],
$dao['author'],
$dao['content'],
$dao['link'],
$dao['date'],
$dao['is_read'],
$dao['is_favorite'],
$dao['tags']
);
if (isset($dao['id'])) {
$entry->_id($dao['id']);
}
$list[] = $entry;
}
unset($listDAO);
return $list;
}
}

View file

@ -0,0 +1,129 @@
<?php
class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
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) '
. 'WHERE 1';
$values = array();
if ($feedId !== false) {
$sql .= ' AND id=?';
$values[] = $feedId;
}
if ($catId !== false) {
$sql .= ' AND 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::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
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::record('SQL error markRead 1: ' . $info[2], Minz_Log::ERROR);
$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::record('SQL error markRead 2: ' . $info[2], Minz_Log::ERROR);
$this->bd->rollBack();
return false;
}
}
$this->bd->commit();
return $affected;
}
}
public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::record($nb . 'Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG);
}
$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);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR);
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
return false;
}
return $affected;
}
public function markReadCat($id, $idMax = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::record($nb . 'Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG);
}
$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);
$stm = $this->bd->prepare($sql);
if (!($stm && $stm->execute($values))) {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR);
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(DATA_PATH . '/' . Minz_Session::param('currentUser', '_') . '.sqlite');
}
}

32
sources/app/Models/Factory.php Executable file
View file

@ -0,0 +1,32 @@
<?php
class FreshRSS_Factory {
public static function createFeedDao() {
$db = Minz_Configuration::dataBase();
if ($db['type'] === 'sqlite') {
return new FreshRSS_FeedDAOSQLite();
} else {
return new FreshRSS_FeedDAO();
}
}
public static function createEntryDao() {
$db = Minz_Configuration::dataBase();
if ($db['type'] === 'sqlite') {
return new FreshRSS_EntryDAOSQLite();
} else {
return new FreshRSS_EntryDAO();
}
}
public static function createStatsDAO() {
$db = Minz_Configuration::dataBase();
if ($db['type'] === 'sqlite') {
return new FreshRSS_StatsDAOSQLite();
} else {
return new FreshRSS_StatsDAO();
}
}
}

325
sources/app/Models/Feed.php Executable file
View file

@ -0,0 +1,325 @@
<?php
class FreshRSS_Feed extends Minz_Model {
private $id = 0;
private $url;
private $category = 1;
private $nbEntries = -1;
private $nbNotRead = -1;
private $entries = null;
private $name = '';
private $website = '';
private $description = '';
private $lastUpdate = 0;
private $priority = 10;
private $pathEntries = '';
private $httpAuth = '';
private $error = false;
private $keep_history = -2;
private $ttl = -2;
private $hash = null;
private $lockPath = '';
public function __construct($url, $validate=true) {
if ($validate) {
$this->_url($url);
} else {
$this->url = $url;
}
}
public function id() {
return $this->id;
}
public function hash() {
if ($this->hash === null) {
$this->hash = hash('crc32b', Minz_Configuration::salt() . $this->url);
}
return $this->hash;
}
public function url() {
return $this->url;
}
public function category() {
return $this->category;
}
public function entries() {
return $this->entries === null ? array() : $this->entries;
}
public function name() {
return $this->name;
}
public function website() {
return $this->website;
}
public function description() {
return $this->description;
}
public function lastUpdate() {
return $this->lastUpdate;
}
public function priority() {
return $this->priority;
}
public function pathEntries() {
return $this->pathEntries;
}
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);
return array(
'username' => $user,
'password' => $pass
);
}
}
public function inError() {
return $this->error;
}
public function keepHistory() {
return $this->keep_history;
}
public function ttl() {
return $this->ttl;
}
public function nbEntries() {
if ($this->nbEntries < 0) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->nbEntries = $feedDAO->countEntries($this->id());
}
return $this->nbEntries;
}
public function nbNotRead() {
if ($this->nbNotRead < 0) {
$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 ($t == '') {
$t = $this->url;
}
file_put_contents($file, $t);
}
}
public static function faviconDelete($hash) {
$path = DATA_PATH . '/favicons/' . $hash;
@unlink($path . '.ico');
@unlink($path . '.txt');
}
public function favicon() {
return Minz_Url::display('/f.php?' . $this->hash());
}
public function _id($value) {
$this->id = $value;
}
public function _url($value, $validate=true) {
$this->hash = null;
if ($validate) {
$value = checkUrl($value);
}
if (empty($value)) {
throw new FreshRSS_BadUrl_Exception($value);
}
$this->url = $value;
}
public function _category($value) {
$value = intval($value);
$this->category = $value >= 0 ? $value : 0;
}
public function _name($value) {
$this->name = $value === null ? '' : $value;
}
public function _website($value, $validate=true) {
if ($validate) {
$value = checkUrl($value);
}
if (empty($value)) {
$value = '';
}
$this->website = $value;
}
public function _description($value) {
$this->description = $value === null ? '' : $value;
}
public function _lastUpdate($value) {
$this->lastUpdate = $value;
}
public function _priority($value) {
$value = intval($value);
$this->priority = $value >= 0 ? $value : 10;
}
public function _pathEntries($value) {
$this->pathEntries = $value;
}
public function _httpAuth($value) {
$this->httpAuth = $value;
}
public function _error($value) {
$this->error = (bool)$value;
}
public function _keepHistory($value) {
$value = intval($value);
$value = min($value, 1000000);
$value = max($value, -2);
$this->keep_history = $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) {
$this->nbEntries = intval($value);
}
public function load($loadDetails = false) {
if ($this->url !== null) {
if (CACHE_PATH === false) {
throw new Minz_FileNotExistException(
'CACHE_PATH',
Minz_Exception::ERROR
);
} else {
$url = htmlspecialchars_decode($this->url, ENT_QUOTES);
if ($this->httpAuth != '') {
$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
}
$feed = customSimplePie();
$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 . ']');
}
if ($loadDetails) {
// 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 == '' ? $this->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 ($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);
}
if (($mtime === true) ||($mtime > $this->lastUpdate)) {
syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url);
$this->loadEntries($feed); // et on charge les articles du flux
} else {
syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url);
$this->entries = array();
}
$feed->__destruct(); //http://simplepie.org/wiki/faq/i_m_getting_memory_leaks
unset($feed);
}
}
}
private 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());
// gestion des tags (catégorie == tag)
$tags_tmp = $item->get_categories();
$tags = array();
if ($tags_tmp !== null) {
foreach ($tags_tmp as $tag) {
$tags[] = html_only_entity_decode($tag->get_label());
}
}
$content = html_only_entity_decode($item->get_content());
$elinks = array();
foreach ($item->get_enclosures() as $enclosure) {
$elink = $enclosure->get_link();
if (empty($elinks[$elink])) {
$elinks[$elink] = '1';
$mime = strtolower($enclosure->get_type());
if (strpos($mime, 'image/') === 0) {
$content .= '<br /><img src="' . $elink . '" alt="" />';
} elseif (strpos($mime, 'audio/') === 0) {
$content .= '<br /><audio src="' . $elink . '" controls="controls" />';
} elseif (strpos($mime, 'video/') === 0) {
$content .= '<br /><video src="' . $elink . '" controls="controls" />';
}
}
}
$entry = new FreshRSS_Entry(
$this->id(),
$item->get_id(),
$title === null ? '' : $title,
$author === null ? '' : html_only_entity_decode($author->name),
$content === null ? '' : $content,
$link === null ? '' : $link,
$date ? $date : time()
);
$entry->_tags($tags);
// permet de récupérer le contenu des flux tronqués
$entry->loadCompleteContent($this->pathEntries());
$entries[] = $entry;
unset($item);
}
$this->entries = $entries;
}
function 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($this->lockPath, 'x')) === false) {
return false;
}
//register_shutdown_function('unlink', $this->lockPath);
@fclose($handle);
return true;
}
function unlock() {
@unlink($this->lockPath);
}
}

388
sources/app/Models/FeedDAO.php Executable file
View file

@ -0,0 +1,388 @@
<?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, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
$stm = $this->bd->prepare($sql);
$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']),
);
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error addFeed: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
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) {
$set = '';
foreach ($valuesTmp as $key => $v) {
$set .= $key . '=?, ';
if ($key == 'httpAuth') {
$valuesTmp[$key] = base64_encode($v);
}
}
$set = substr($set, 0, -2);
$sql = 'UPDATE `' . $this->prefix . 'feed` SET ' . $set . ' WHERE id=?';
$stm = $this->bd->prepare($sql);
foreach ($valuesTmp as $v) {
$values[] = $v;
}
$values[] = $id;
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error updateFeed: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function updateLastUpdate($id, $inError = 0, $updateCache = true) {
if ($updateCache) {
$sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
. 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
. 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
. 'lastUpdate=?, error=? '
. 'WHERE id=?';
} else {
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET lastUpdate=?, error=? '
. 'WHERE id=?';
}
$values = array(
time(),
$inError,
$id,
);
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error updateLastUpdate: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function changeCategory($idOldCat, $idNewCat) {
$catDAO = new FreshRSS_CategoryDAO();
$newCat = $catDAO->searchById($idNewCat);
if (!$newCat) {
$newCat = $catDAO->getDefault();
}
$sql = 'UPDATE `' . $this->prefix . 'feed` SET category=? WHERE category=?';
$stm = $this->bd->prepare($sql);
$values = array(
$newCat->id(),
$idOldCat
);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error changeCategory: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function deleteFeed($id) {
$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE id=?';
$stm = $this->bd->prepare($sql);
$values = array($id);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error deleteFeed: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function deleteFeedByCategory($id) {
$sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE category=?';
$stm = $this->bd->prepare($sql);
$values = array($id);
if ($stm && $stm->execute($values)) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error deleteFeedByCategory: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function searchById($id) {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE id=?';
$stm = $this->bd->prepare($sql);
$values = array($id);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$feed = self::daoToFeed($res);
if (isset($feed[$id])) {
return $feed[$id];
} else {
return null;
}
}
public function searchByUrl($url) {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE url=?';
$stm = $this->bd->prepare($sql);
$values = array($url);
$stm->execute($values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$feed = current(self::daoToFeed($res));
if (isset($feed)) {
return $feed;
} else {
return null;
}
}
public function listFeeds() {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name';
$stm = $this->bd->prepare($sql);
$stm->execute();
return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
}
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;
}
public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
if ($defaultCacheDuration < 0) {
$defaultCacheDuration = 2147483647;
}
$sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl '
. 'FROM `' . $this->prefix . 'feed` '
. 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) '
. 'ORDER BY lastUpdate';
$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));
}
public function listByCategory($cat) {
$sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE category=? ORDER BY name';
$stm = $this->bd->prepare($sql);
$values = array($cat);
$stm->execute($values);
return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
}
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);
return $res[0]['count'];
}
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);
return $res[0]['count'];
}
public function updateCachedValues() { //For one single feed, call updateLastUpdate($id)
$sql = 'UPDATE `' . $this->prefix . 'feed` f '
. 'INNER JOIN ('
. 'SELECT e.id_feed, '
. 'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, '
. 'COUNT(e.id) AS nbEntries '
. '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);
if ($stm && $stm->execute()) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
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 == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR);
$this->bd->rollBack();
return false;
}
$affected = $stm->rowCount();
$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::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR);
$this->bd->rollBack();
return false;
}
$this->bd->commit();
return $affected;
}
public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) just after
$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
. 'WHERE id_feed = :id_feed AND id <= :id_max AND is_favorite=0 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 yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
$stm = $this->bd->prepare($sql);
$id_max = intval($date_min) . '000000';
$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()) {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record('SQL error cleanOldEntries: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public static function daoToFeed($listDAO, $catID = null) {
$list = array();
if (!is_array($listDAO)) {
$listDAO = array($listDAO);
}
foreach ($listDAO as $key => $dao) {
if (!isset($dao['name'])) {
continue;
}
if (isset($dao['id'])) {
$key = $dao['id'];
}
if ($catID === null) {
$category = isset($dao['category']) ? $dao['category'] : 0;
} else {
$category = $catID ;
}
$myFeed = new FreshRSS_Feed(isset($dao['url']) ? $dao['url'] : '', false);
$myFeed->_category($category);
$myFeed->_name($dao['name']);
$myFeed->_website(isset($dao['website']) ? $dao['website'] : '', false);
$myFeed->_description(isset($dao['description']) ? $dao['description'] : '');
$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->_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']);
}
$list[$key] = $myFeed;
}
return $list;
}
}

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::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR);
return false;
}
}
}

26
sources/app/Models/Log.php Executable file
View file

@ -0,0 +1,26 @@
<?php
class FreshRSS_Log extends Minz_Model {
private $date;
private $level;
private $information;
public function date () {
return $this->date;
}
public function level () {
return $this->level;
}
public function info () {
return $this->information;
}
public function _date ($date) {
$this->date = $date;
}
public function _level ($level) {
$this->level = $level;
}
public function _info ($information) {
$this->information = $information;
}
}

25
sources/app/Models/LogDAO.php Executable file
View file

@ -0,0 +1,25 @@
<?php
class FreshRSS_LogDAO {
public static function lines() {
$logs = array ();
$handle = @fopen(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
if (preg_match ('/^\[([^\[]+)\] \[([^\[]+)\] --- (.*)$/', $line, $matches)) {
$myLog = new FreshRSS_Log ();
$myLog->_date ($matches[1]);
$myLog->_level ($matches[2]);
$myLog->_info ($matches[3]);
$logs[] = $myLog;
}
}
fclose($handle);
}
return array_reverse($logs);
}
public static function truncate() {
file_put_contents(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log', '');
}
}

44
sources/app/Models/Share.php Executable file
View file

@ -0,0 +1,44 @@
<?php
class FreshRSS_Share {
static public function generateUrl($options, $selected, $link, $title) {
$share = $options[$selected['type']];
$matches = array(
'~URL~',
'~TITLE~',
'~LINK~',
);
$replaces = array(
$selected['url'],
self::transformData($title, self::getTransform($share, 'title')),
self::transformData($link, self::getTransform($share, 'link')),
);
$url = str_replace($matches, $replaces, $share['url']);
return $url;
}
static private function transformData($data, $transform) {
if (!is_array($transform)) {
return $data;
}
if (count($transform) === 0) {
return $data;
}
foreach ($transform as $action) {
$data = call_user_func($action, $data);
}
return $data;
}
static private function getTransform($options, $type) {
$transform = $options['transform'];
if (array_key_exists($type, $transform)) {
return $transform[$type];
}
return $transform;
}
}

207
sources/app/Models/StatsDAO.php Executable file
View file

@ -0,0 +1,207 @@
<?php
class FreshRSS_StatsDAO extends Minz_ModelPdo {
const ENTRY_COUNT_PERIOD = 30;
/**
* Calculates entry repartition for all feeds and for main stream.
* The repartition includes:
* - total entries
* - read entries
* - unread entries
* - favorite entries
*
* @return type
*/
public function calculateEntryRepartition() {
$repartition = array();
// Generates the repartition for the main stream of entry
$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
WHERE e.id_feed = f.id
AND f.priority = 10
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;
}
/**
* Calculates entry count per day on a 30 days period.
* Returns the result as a JSON string.
*
* @return string
*/
public function calculateEntryCount() {
$count = $this->initEntryCountArray();
$period = self::ENTRY_COUNT_PERIOD;
// Get stats per day for the last 30 days
$sql = <<<SQL
SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day,
COUNT(1) AS count
FROM {$this->prefix}entry AS e
WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d')
GROUP BY day
ORDER BY day ASC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
foreach ($res as $value) {
$count[$value['day']] = (int) $value['count'];
}
return $this->convertToSerie($count);
}
/**
* Initialize an array for the entry count.
*
* @return array
*/
protected function initEntryCountArray() {
return array_map(function () {
return 0;
}, array_flip(range(-self::ENTRY_COUNT_PERIOD, -1)));
}
/**
* Calculates feed count per category.
* Returns the result as a JSON string.
*
* @return string
*/
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
WHERE c.id = f.category
GROUP BY label
ORDER BY data DESC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $this->convertToPieSerie($res);
}
/**
* Calculates entry count per category.
* Returns the result as a JSON string.
*
* @return string
*/
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
WHERE c.id = f.category
AND f.id = e.id_feed
GROUP BY label
ORDER BY data DESC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return $this->convertToPieSerie($res);
}
/**
* Calculates the 10 top feeds based on their number of entries
*
* @return array
*/
public function calculateTopFeed() {
$sql = <<<SQL
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
WHERE c.id = f.category
AND f.id = e.id_feed
GROUP BY f.id
ORDER BY count DESC
LIMIT 10
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
return $stm->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Calculates the last publication date for each feed
*
* @return array
*/
public function calculateFeedLastDate() {
$sql = <<<SQL
SELECT MAX(f.name) AS name
, MAX(date) AS last_date
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);
}
protected function convertToSerie($data) {
$serie = array();
foreach ($data as $key => $value) {
$serie[] = array($key, $value);
}
return json_encode($serie);
}
protected function convertToPieSerie($data) {
$serie = array();
foreach ($data as $value) {
$value['data'] = array(array(0, (int) $value['data']));
$serie[] = $value;
}
return json_encode($serie);
}
}

View file

@ -0,0 +1,37 @@
<?php
class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
/**
* Calculates entry count per day on a 30 days period.
* Returns the result as a JSON string.
*
* @return string
*/
public function calculateEntryCount() {
$count = $this->initEntryCountArray();
$period = parent::ENTRY_COUNT_PERIOD;
// Get stats per day for the last 30 days
$sql = <<<SQL
SELECT round(julianday(e.date, 'unixepoch') - julianday('now')) AS day,
COUNT(1) AS count
FROM {$this->prefix}entry AS e
WHERE strftime('%Y%m%d', e.date, 'unixepoch')
BETWEEN strftime('%Y%m%d', 'now', '-{$period} days')
AND strftime('%Y%m%d', 'now', '-1 day')
GROUP BY day
ORDER BY day ASC
SQL;
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
foreach ($res as $value) {
$count[(int)$value['day']] = (int) $value['count'];
}
return $this->convertToSerie($count);
}
}

120
sources/app/Models/Themes.php Executable file
View file

@ -0,0 +1,120 @@
<?php
class FreshRSS_Themes extends Minz_Model {
private static $themesUrl = '/themes/';
private static $defaultIconsUrl = '/themes/icons/';
public static $defaultTheme = 'Origine';
public static function getList() {
return array_values(array_diff(
scandir(PUBLIC_PATH . self::$themesUrl),
array('..', '.')
));
}
public static function get() {
$themes_list = self::getList();
$list = array();
foreach ($themes_list as $theme_dir) {
$theme = self::get_infos($theme_dir);
if ($theme) {
$list[$theme_dir] = $theme;
}
}
return $list;
}
public static function get_infos($theme_id) {
$theme_dir = PUBLIC_PATH . self::$themesUrl . $theme_id ;
if (is_dir($theme_dir)) {
$json_filename = $theme_dir . '/metadata.json';
if (file_exists($json_filename)) {
$content = file_get_contents($json_filename);
$res = json_decode($content, true);
if ($res &&
!empty($res['name']) &&
isset($res['files']) &&
is_array($res['files'])) {
$res['id'] = $theme_id;
return $res;
}
}
}
return false;
}
private static $themeIconsUrl;
private static $themeIcons;
public static function load($theme_id) {
$infos = self::get_infos($theme_id);
if (!$infos) {
if ($theme_id !== self::$defaultTheme) { //Fall-back to default theme
return self::load(self::$defaultTheme);
}
$themes_list = self::getList();
if (!empty($themes_list)) {
if ($theme_id !== $themes_list[0]) { //Fall-back to first theme
return self::load($themes_list[0]);
}
}
return false;
}
self::$themeIconsUrl = self::$themesUrl . $theme_id . '/icons/';
self::$themeIcons = is_dir(PUBLIC_PATH . self::$themeIconsUrl) ? array_fill_keys(array_diff(
scandir(PUBLIC_PATH . self::$themeIconsUrl),
array('..', '.')
), 1) : array();
return $infos;
}
public static function icon($name, $urlOnly = false) {
static $alts = array(
'add' => '✚',
'all' => '☰',
'bookmark' => '★',
'bookmark-add' => '✚',
'category' => '☷',
'category-white' => '☷',
'close' => '❌',
'configure' => '⚙',
'down' => '▽',
'favorite' => '★',
'help' => 'ⓘ',
'icon' => '⊚',
'key' => '⚿',
'link' => '↗',
'login' => '🔒',
'logout' => '🔓',
'next' => '⏩',
'non-starred' => '☆',
'prev' => '⏪',
'read' => '☑',
'rss' => '☄',
'unread' => '☐',
'refresh' => '🔃', //↻
'search' => '🔍',
'share' => '♺',
'starred' => '★',
'tag' => '⚐',
'up' => '△',
'view-normal' => '☰',
'view-global' => '☷',
'view-reader' => '☕',
);
if (!isset($alts[$name])) {
return '';
}
$url = $name . '.svg';
$url = isset(self::$themeIcons[$url]) ? (self::$themeIconsUrl . $url) :
(self::$defaultIconsUrl . $url);
return $urlOnly ? Minz_Url::display($url) :
'<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alts[$name] . '" />';
}
}
function _i($icon, $url_only = false) {
return FreshRSS_Themes::icon($icon, $url_only);
}

47
sources/app/Models/UserDAO.php Executable file
View file

@ -0,0 +1,47 @@
<?php
class FreshRSS_UserDAO extends Minz_ModelPdo {
public function createUser($username) {
$db = Minz_Configuration::dataBase();
require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
if (defined('SQL_CREATE_TABLES')) {
$sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', Minz_Translate::t('default_category'));
$stm = $c->prepare($sql);
$ok = $stm && $stm->execute();
} else {
global $SQL_CREATE_TABLES;
if (is_array($SQL_CREATE_TABLES)) {
$ok = true;
foreach ($SQL_CREATE_TABLES as $instruction) {
$sql = sprintf($instruction, '', Minz_Translate::t('default_category'));
$stm = $c->prepare($sql);
$ok &= ($stm && $stm->execute());
}
}
}
if ($ok) {
return true;
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
return false;
}
}
public function deleteUser($username) {
$db = Minz_Configuration::dataBase();
require_once(APP_PATH . '/SQL/sql.' . $db['type'] . '.php');
$sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_');
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute()) {
return true;
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR);
return false;
}
}
}

View file

@ -0,0 +1,61 @@
<?php
define('SQL_CREATE_TABLES', '
CREATE TABLE IF NOT EXISTS `%1$scategory` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`name`) -- v0.7
) DEFAULT CHARACTER SET utf8 COLLATE utf8_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(255) NOT NULL,
`website` varchar(255) CHARACTER SET latin1,
`description` text,
`lastUpdate` int(11) DEFAULT 0,
`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 utf8 COLLATE utf8_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),
`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
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
ENGINE = INNODB;
INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
');
define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
define('SQL_SHOW_TABLES', 'SHOW tables;');

View file

@ -0,0 +1,58 @@
<?php
$SQL_CREATE_TABLES = array(
'CREATE TABLE IF NOT EXISTS `%1$scategory` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` varchar(255) NOT NULL,
UNIQUE (`name`)
);',
'CREATE TABLE IF NOT EXISTS `%1$sfeed` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`url` varchar(511) NOT NULL,
`%1$scategory` SMALLINT DEFAULT 0,
`name` varchar(255) NOT NULL,
`website` varchar(255),
`description` text,
`lastUpdate` int(11) DEFAULT 0,
`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 (`%1$scategory`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
UNIQUE (`url`)
);',
'CREATE INDEX IF NOT EXISTS feed_name_index ON `%1$sfeed`(`name`);',
'CREATE INDEX IF NOT EXISTS feed_priority_index ON `%1$sfeed`(`priority`);',
'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `%1$sfeed`(`keep_history`);',
'CREATE TABLE IF NOT EXISTS `%1$sentry` (
`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),
`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 `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE (`id_feed`,`guid`)
);',
'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `%1$sentry`(`is_favorite`);',
'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);',
'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");',
);
define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
define('SQL_SHOW_TABLES', 'SELECT name FROM sqlite_master WHERE type="table"');

View file

@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS `YnoUser_category` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`name`) -- v0.7
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `YnoUser_feed` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
`url` varchar(511) CHARACTER SET latin1 NOT NULL,
`category` SMALLINT DEFAULT 0, -- v0.7
`name` varchar(255) NOT NULL,
`website` varchar(255) CHARACTER SET latin1,
`description` text,
`lastUpdate` int(11) DEFAULT 0,
`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 `YnoUser_category`(`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 utf8 COLLATE utf8_unicode_ci
ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `YnoUser_entry` (
`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),
`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 `YnoUser_feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE KEY (`id_feed`,`guid`), -- v0.7
INDEX (`is_favorite`), -- v0.7
INDEX (`is_read`) -- v0.7
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
ENGINE = INNODB;
INSERT IGNORE INTO `YnoUser_category` (id, name) VALUES(1, "Uncategorized");

View file

@ -0,0 +1,54 @@
<?php
require(dirname(__FILE__) . '/../constants.php');
require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
session_cache_limiter('');
ob_implicit_flush(false);
ob_start();
echo 'Results: ', "\n"; //Buffered
Minz_Configuration::init();
$users = listUsers();
shuffle($users); //Process users in random order
array_unshift($users, Minz_Configuration::defaultUser()); //But always start with admin
$users = array_unique($users);
foreach ($users as $myUser) {
syslog(LOG_INFO, 'FreshRSS actualize ' . $myUser);
if (defined('STDOUT')) {
fwrite(STDOUT, 'Actualize ' . $myUser . "...\n"); //Unbuffered
}
echo $myUser, ' '; //Buffered
$_GET['c'] = 'feed';
$_GET['a'] = 'actualize';
$_GET['ajax'] = 1;
$_GET['force'] = true;
$_SERVER['HTTP_HOST'] = '';
$freshRSS = new FreshRSS();
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!');
if (defined('STDERR')) {
fwrite(STDERR, 'Write access problem in ' . LOG_PATH . '/*.log!' . "\n");
}
}
Minz_Session::unset_session(true);
Minz_ModelPdo::clean();
}
syslog(LOG_INFO, 'FreshRSS actualize done.');
if (defined('STDOUT')) {
fwrite(STDOUT, 'Done.' . "\n");
}
echo 'End.', "\n";
ob_end_flush();

384
sources/app/i18n/en.php Executable file
View file

@ -0,0 +1,384 @@
<?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',
'queries' => 'User queries',
'query_search' => 'Search for "%s"',
'query_order_asc' => 'Display oldest articles first',
'query_order_desc' => 'Display newest articles first',
'query_get_category' => 'Display "%s" category',
'query_get_feed' => 'Display "%s" feed',
'query_get_all' => 'Display all articles',
'query_get_favorite' => 'Display favorite articles',
'query_state_0' => 'Display all articles',
'query_state_1' => 'Display read articles',
'query_state_2' => 'Display unread articles',
'query_state_3' => 'Display all articles',
'query_state_4' => 'Display favorite articles',
'query_state_5' => 'Display read favorite articles',
'query_state_6' => 'Display unread favorite articles',
'query_state_7' => 'Display favorite articles',
'query_state_8' => 'Display not favorite articles',
'query_state_9' => 'Display read not favorite articles',
'query_state_10' => 'Display unread not favorite articles',
'query_state_11' => 'Display not favorite articles',
'query_state_12' => 'Display all articles',
'query_state_13' => 'Display read articles',
'query_state_14' => 'Display unread articles',
'query_state_15' => 'Display all articles',
'query_number' => 'Query n°%d',
'add_query' => 'Add a query',
'no_query' => 'You havent created any user query yet.',
'query_filter' => 'Filter applied:',
'no_query_filter' => 'No filter',
'about' => 'About',
'stats' => 'Statistics',
'stats_idle' => 'Idle feeds',
'stats_main' => 'Main statistics',
'last_week' => 'Last week',
'last_month' => 'Last month',
'last_3_month' => 'Last three months',
'last_6_month' => 'Last six months',
'last_year' => 'Last year',
'your_rss_feeds' => 'Your RSS feeds',
'add_rss_feed' => 'Add a RSS feed',
'no_rss_feed' => 'No RSS feed',
'import_export' => 'Import / export',
'bookmark' => 'Subscribe (FreshRSS bookmark)',
'subscription_management' => 'Subscriptions management',
'main_stream' => 'Main stream',
'all_feeds' => 'All feeds',
'favorite_feeds' => 'Favourites (%s)',
'not_read' => '%d unread',
'not_reads' => '%d unread',
'filter' => 'Filter',
'see_website' => 'See website',
'administration' => 'Manage',
'actualize' => 'Actualize',
'mark_read' => 'Mark as read',
'mark_favorite' => 'Mark as favourite',
'mark_all_read' => 'Mark all as read',
'mark_feed_read' => 'Mark feed as read',
'mark_cat_read' => 'Mark category as read',
'before_one_day' => 'Before one day',
'before_one_week' => 'Before one week',
'display' => 'Display',
'normal_view' => 'Normal view',
'reader_view' => 'Reading view',
'global_view' => 'Global view',
'rss_view' => 'RSS feed',
'show_all_articles' => 'Show all articles',
'show_not_reads' => 'Show only unread',
'show_read' => 'Show only read',
'show_favorite' => 'Show only favorites',
'show_not_favorite' => 'Show all but favorites',
'older_first' => 'Oldest first',
'newer_first' => 'Newer first',
// Pagination
'first' => 'First',
'previous' => 'Previous',
'next' => 'Next',
'last' => 'Last',
// CONTROLLERS
'article_published_on' => 'This article originally appeared on <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_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. <a href="%s">Check FressRSS logs</a> 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.',
'focus_search' => 'Access search box',
'file_to_import' => 'File to import<br />(OPML, Json or Zip)',
'import' => 'Import',
'export' => 'Export',
'export_opml' => 'Export list of feeds (OPML)',
'export_starred' => 'Export your favourites',
'starred_list' => 'List of favourite articles',
'feed_list' => 'List of %s articles',
'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',
'ttl' => 'Do not automatically refresh more often than',
'categorize' => 'Store in a category',
'truncate' => 'Delete all articles',
'advanced' => 'Advanced',
'show_in_all_flux' => 'Show in main stream',
'yes' => 'Yes',
'no' => 'No',
'css_path_on_website' => 'Articles CSS path on original website',
'retrieve_truncated_feeds' => 'Retrieves truncated RSS feeds (attention, requires more time!)',
'http_authentication' => 'HTTP Authentication',
'http_username' => 'HTTP username',
'http_password' => 'HTTP password',
'blank_to_disable' => 'Leave blank to disable',
'share_name' => 'Share name to display',
'share_url' => 'Share URL to use',
'not_yet_implemented' => 'Not yet implemented',
'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds',
'no_selected_feed' => 'No feed selected.',
'think_to_add' => '<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>',
'password_api' => 'Password API<br /><small>(e.g., for mobile apps)</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',
'unsafe_autologin' => 'Allow unsafe automatic login using the format: ',
'api_enabled' => 'Allow <abbr>API</abbr> access <small>(required for mobile apps)</small>',
'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',
'display_configuration' => 'Display',
'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)',
'article_icons' => 'Article icons',
'top_line' => 'Top line',
'bottom_line' => 'Bottom line',
'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
'sticky_post' => 'Stick the article to the top when opened',
'reading_confirm' => 'Display a confirmation dialog on “mark all as read” actions',
'auto_read_when' => 'Mark article as read…',
'article_viewed' => 'when article is viewed',
'article_open_on_website' => 'when article is opened on its original website',
'scroll' => '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',
'content_width' => 'Content width',
'width_thin' => 'Thin',
'width_medium' => 'Medium',
'width_large' => 'Large',
'width_no_limit' => 'No limit',
'more_information' => 'More information',
'activate_sharing' => 'Activate sharing',
'shaarli' => 'Shaarli',
'blogotext' => 'Blogotext',
'wallabag' => 'wallabag',
'diaspora' => 'Diaspora*',
'twitter' => 'Twitter',
'g+' => 'Google+',
'facebook' => 'Facebook',
'email' => 'Email',
'print' => 'Print',
'article' => 'Article',
'title' => 'Title',
'author' => 'Author',
'publication_date' => 'Date of publication',
'by' => 'by',
'load_more' => 'Load more articles',
'nothing_to_load' => 'There are no more articles',
'rss_feeds_of' => 'RSS feed of %s',
'refresh' => 'Refresh',
'no_feed_to_refresh' => 'There is no feed to refresh…',
'today' => 'Today',
'yesterday' => 'Yesterday',
'before_yesterday' => 'Before yesterday',
'new_article' => 'There are new available articles, click to refresh the page.',
'by_author' => 'By <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',
);

384
sources/app/i18n/fr.php Executable file
View file

@ -0,0 +1,384 @@
<?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',
'queries' => 'Filtres utilisateurs',
'query_search' => 'Recherche de "%s"',
'query_order_asc' => 'Afficher les articles les plus anciens en premier',
'query_order_desc' => 'Afficher les articles les plus récents en premier',
'query_get_category' => 'Afficher la catégorie "%s"',
'query_get_feed' => 'Afficher le flux "%s"',
'query_get_all' => 'Afficher tous les articles',
'query_get_favorite' => 'Afficher les articles favoris',
'query_state_0' => 'Afficher tous les articles',
'query_state_1' => 'Afficher les articles lus',
'query_state_2' => 'Afficher les articles non lus',
'query_state_3' => 'Afficher tous les articles',
'query_state_4' => 'Afficher les articles favoris',
'query_state_5' => 'Afficher les articles lus et favoris',
'query_state_6' => 'Afficher les articles non lus et favoris',
'query_state_7' => 'Afficher les articles favoris',
'query_state_8' => 'Afficher les articles non favoris',
'query_state_9' => 'Afficher les articles lus et non favoris',
'query_state_10' => 'Afficher les articles non lus et non favoris',
'query_state_11' => 'Afficher les articles non favoris',
'query_state_12' => 'Afficher tous les articles',
'query_state_13' => 'Afficher les articles lus',
'query_state_14' => 'Afficher les articles non lus',
'query_state_15' => 'Afficher tous les articles',
'query_number' => 'Filtre n°%d',
'add_query' => 'Créer un filtre',
'no_query' => 'Vous navez pas encore créé de filtre.',
'query_filter' => 'Filtres appliqués :',
'no_query_filter' => 'Aucun filtre appliqué',
'about' => 'À propos',
'stats' => 'Statistiques',
'stats_idle' => 'Flux inactifs',
'stats_main' => 'Statistiques principales',
'last_week' => 'La dernière semaine',
'last_month' => 'Le dernier mois',
'last_3_month' => 'Les derniers trois mois',
'last_6_month' => 'Les derniers six mois',
'last_year' => 'La dernière année',
'your_rss_feeds' => 'Vos flux RSS',
'add_rss_feed' => 'Ajouter un flux RSS',
'no_rss_feed' => 'Aucun flux RSS',
'import_export' => 'Importer / exporter',
'bookmark' => 'Sabonner (bookmark FreshRSS)',
'subscription_management' => 'Gestion des abonnements',
'main_stream' => 'Flux principal',
'all_feeds' => 'Tous les flux',
'favorite_feeds' => 'Favoris (%s)',
'not_read' => '%d non lu',
'not_reads' => '%d non lus',
'filter' => 'Filtrer',
'see_website' => 'Voir le site',
'administration' => 'Gérer',
'actualize' => 'Actualiser',
'mark_read' => 'Marquer comme lu',
'mark_favorite' => 'Mettre en favori',
'mark_all_read' => 'Tout marquer comme lu',
'mark_feed_read' => 'Marquer le flux comme lu',
'mark_cat_read' => 'Marquer la catégorie comme lue',
'before_one_day' => 'Antérieurs à 1 jour',
'before_one_week' => 'Antérieurs à 1 semaine',
'display' => 'Affichage',
'normal_view' => 'Vue normale',
'reader_view' => 'Vue lecture',
'global_view' => 'Vue globale',
'rss_view' => 'Flux RSS',
'show_all_articles' => 'Afficher tous les articles',
'show_not_reads' => 'Afficher les non lus',
'show_read' => 'Afficher les lus',
'show_favorite' => 'Afficher les favoris',
'show_not_favorite' => 'Afficher tout sauf les favoris',
'older_first' => 'Plus anciens en premier',
'newer_first' => 'Plus récents en premier',
// Pagination
'first' => 'Début',
'previous' => 'Précédent',
'next' => 'Suivant',
'last' => 'Fin',
// CONTROLLERS
'article_published_on' => 'Article publié initialement sur <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_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 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.',
'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' => 'Sil ny a quun mode de partage, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.',
'focus_search' => 'Accéder à la recherche',
'file_to_import' => 'Fichier à importer<br />(OPML, Json ou Zip)',
'import' => 'Importer',
'export' => 'Exporter',
'export_opml' => 'Exporter la liste des flux (OPML)',
'export_starred' => 'Exporter les favoris',
'starred_list' => 'Liste des articles favoris',
'feed_list' => 'Liste des articles de %s',
'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',
'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
'categorize' => 'Ranger dans une catégorie',
'truncate' => 'Supprimer tous les articles',
'advanced' => 'Avancé',
'show_in_all_flux' => 'Afficher dans le flux principal',
'yes' => 'Oui',
'no' => 'Non',
'css_path_on_website' => 'Sélecteur CSS des articles sur le site 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',
'share_name' => 'Nom du partage à afficher',
'share_url' => 'URL du partage à utiliser',
'not_yet_implemented' => 'Pas encore implémenté',
'access_protected_feeds' => 'La connexion permet 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>',
'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</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',
'unsafe_autologin' => 'Autoriser les connexion automatiques non-sûres au format : ',
'api_enabled' => 'Autoriser laccès par <abbr>API</abbr> <small>(nécessaire pour les applis mobiles)</small>',
'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',
'display_configuration' => 'Affichage',
'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)',
'article_icons' => 'Icônes darticle',
'top_line' => 'Ligne du haut',
'bottom_line' => 'Ligne du bas',
'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images',
'sticky_post' => 'Aligner larticle en haut quand il est ouvert',
'reading_confirm' => 'Afficher une confirmation lors des actions “marquer tout comme lu”',
'auto_read_when' => 'Marquer un article comme lu…',
'article_viewed' => 'lorsque larticle est affiché',
'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',
'content_width' => 'Largeur du contenu',
'width_thin' => 'Fine',
'width_medium' => 'Moyenne',
'width_large' => 'Large',
'width_no_limit' => 'Pas de limite',
'more_information' => 'Plus dinformations',
'activate_sharing' => 'Activer le partage',
'shaarli' => 'Shaarli',
'blogotext' => 'Blogotext',
'wallabag' => 'wallabag',
'diaspora' => 'Diaspora*',
'twitter' => 'Twitter',
'g+' => 'Google+',
'facebook' => 'Facebook',
'email' => 'Courriel',
'print' => 'Imprimer',
'article' => 'Article',
'title' => 'Titre',
'author' => 'Auteur',
'publication_date' => 'Date de publication',
'by' => 'par',
'load_more' => 'Charger plus 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',
);

67
sources/app/i18n/install.en.php Executable file
View file

@ -0,0 +1,67 @@
<?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 any file and database backup created during the update process.<br />You may choose to skip this step by deleting <kbd>./data/do-install.txt</kbd> manually.',
'finish_installation' => 'Complete installation',
'install_not_deleted' => 'Something went wrong; you must delete the file <em>%s</em> manually.',
);

66
sources/app/i18n/install.fr.php Executable file
View file

@ -0,0 +1,66 @@
<?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 les fichiers ainsi que déventuelles copies de base de données créés durant le processus de mise à jour.<br />Vous pouvez choisir de sauter cette étape en supprimant <kbd>./data/do-install.txt</kbd> manuellement.',
'finish_installation' => 'Terminer linstallation',
'install_not_deleted' => 'Quelque chose sest mal passé, vous devez supprimer le fichier <em>%s</em> à la main.',
);

13
sources/app/index.html Executable file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=/" />
<title>Redirection</title>
<meta name="robots" content="noindex" />
</head>
<body>
<p><a href="/">Redirection</a></p>
</body>
</html>

1124
sources/app/install.php Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
<ul class="nav nav-list aside">
<li class="nav-header"><?php echo Minz_Translate::t ('configuration'); ?></li>
<li class="item<?php echo Minz_Request::actionName () == 'display' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('display_configuration'); ?></a>
</li>
<li class="item<?php echo Minz_Request::actionName () == 'reading' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'reading'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a>
</li>
<li class="item<?php echo Minz_Request::actionName () == 'archiving' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'archiving'); ?>"><?php echo Minz_Translate::t ('archiving_configuration'); ?></a>
</li>
<li class="item<?php echo Minz_Request::actionName () == 'sharing' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'sharing'); ?>"><?php echo Minz_Translate::t ('sharing'); ?></a>
</li>
<li class="item<?php echo Minz_Request::actionName () == 'shortcut' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Minz_Translate::t ('shortcuts'); ?></a>
</li>
<li class="item<?php echo Minz_Request::actionName () == 'queries' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'queries'); ?>"><?php echo Minz_Translate::t ('queries'); ?></a>
</li>
<li class="separator"></li>
<li class="item<?php echo Minz_Request::actionName () == 'users' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'users'); ?>"><?php echo Minz_Translate::t ('users'); ?></a>
</li>
</ul>

View file

@ -0,0 +1,75 @@
<ul class="nav nav-list aside aside_feed">
<li class="nav-header"><?php echo Minz_Translate::t ('your_rss_feeds'); ?></li>
<li class="nav-form"><form id="add_rss" method="post" action="<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'add')); ?>" autocomplete="off">
<div class="stick">
<input type="url" name="url_rss" placeholder="<?php echo Minz_Translate::t ('add_rss_feed'); ?>" />
<div class="dropdown">
<div id="dropdown-cat" class="dropdown-target"></div>
<a class="dropdown-toggle btn" href="#dropdown-cat"><?php echo FreshRSS_Themes::icon('down'); ?></a>
<ul class="dropdown-menu">
<li class="dropdown-close"><a href="#close"></a></li>
<li class="dropdown-header"><?php echo Minz_Translate::t ('category'); ?></li>
<li class="input">
<select name="category" id="category">
<?php foreach ($this->categories as $cat) { ?>
<option value="<?php echo $cat->id (); ?>"<?php echo $cat->id () == 1 ? ' selected="selected"' : ''; ?>>
<?php echo $cat->name (); ?>
</option>
<?php } ?>
<option value="nc"><?php echo Minz_Translate::t ('new_category'); ?></option>
</select>
</li>
<li class="input" style="display:none">
<input type="text" name="new_category[name]" id="new_category_name" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('new_category'); ?>" />
</li>
<li class="separator"></li>
<li class="dropdown-header"><?php echo Minz_Translate::t ('http_authentication'); ?></li>
<li class="input">
<input type="text" name="http_user" id="http_user_add" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('username'); ?>" />
</li>
<li class="input">
<input type="password" name="http_pass" id="http_pass_add" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('password'); ?>" />
</li>
</ul>
</div>
<button class="btn" type="submit"><?php echo FreshRSS_Themes::icon('add'); ?></button>
</div>
</form></li>
<li class="item">
<a onclick="return false;" href="javascript:(function(){var%20url%20=%20location.href;window.open('<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&amp;url_rss='+encodeURIComponent(url), '_blank');})();">
<?php echo Minz_Translate::t('bookmark'); ?>
</a>
</li>
<li class="item<?php echo Minz_Request::controllerName () == 'importExport' ? ' active' : ''; ?>">
<a href="<?php echo _url ('importExport', 'index'); ?>"><?php echo Minz_Translate::t ('import_export'); ?></a>
</li>
<li class="item<?php echo Minz_Request::actionName () == 'categorize' ? ' active' : ''; ?>">
<a href="<?php echo _url ('configure', 'categorize'); ?>"><?php echo Minz_Translate::t ('categories_management'); ?></a>
</li>
<li class="separator"></li>
<?php if (!empty ($this->feeds)) { ?>
<?php foreach ($this->feeds as $feed) { ?>
<?php $nbEntries = $feed->nbEntries (); ?>
<li class="item<?php echo (isset($this->flux) && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>">
<a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>">
<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" />
<?php echo $feed->name (); ?>
</a>
</li>
<?php } ?>
<?php } else { ?>
<li class="item disable"><?php echo Minz_Translate::t ('no_rss_feed'); ?></li>
<?php } ?>
</ul>

View file

@ -0,0 +1,88 @@
<div class="aside aside_flux" id="aside_flux">
<a class="toggle_aside" href="#close"><?php echo FreshRSS_Themes::icon('close'); ?></a>
<ul class="categories">
<?php if ($this->loginOk) { ?>
<li>
<div class="stick">
<a class="btn btn-important" href="<?php echo _url ('configure', 'feed'); ?>"><?php echo Minz_Translate::t ('subscription_management'); ?></a>
<a class="btn btn-important" href="<?php echo _url ('configure', 'categorize'); ?>" title="<?php echo Minz_Translate::t ('categories_management'); ?>"><?php echo FreshRSS_Themes::icon('category-white'); ?></a>
</div>
</li>
<?php } elseif (Minz_Configuration::needsLogin()) { ?>
<li><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about_freshrss'); ?></a></li>
<?php } ?>
<?php
$arUrl = array('c' => 'index', 'a' => 'index', 'params' => array());
if ($this->conf->view_mode !== Minz_Request::param('output', 'normal')) {
$arUrl['params']['output'] = 'normal';
}
?>
<li>
<div class="category all<?php echo $this->get_c == 'a' ? ' active' : ''; ?>">
<a data-unread="<?php echo formatNumber($this->nb_not_read); ?>" class="btn<?php echo $this->get_c == 'a' ? ' active' : ''; ?>" href="<?php echo Minz_Url::display($arUrl); ?>">
<?php echo FreshRSS_Themes::icon('all'); ?>
<?php echo Minz_Translate::t ('main_stream'); ?>
</a>
</div>
</li>
<li>
<div class="category favorites<?php echo $this->get_c == 's' ? ' active' : ''; ?>">
<a data-unread="<?php echo formatNumber($this->nb_favorites['unread']); ?>" class="btn<?php echo $this->get_c == 's' ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 's'; echo Minz_Url::display($arUrl); ?>">
<?php echo FreshRSS_Themes::icon('bookmark'); ?>
<?php echo Minz_Translate::t('favorite_feeds', formatNumber($this->nb_favorites['all'])); ?>
</a>
</div>
</li>
<?php
foreach ($this->cat_aside as $cat) {
$feeds = $cat->feeds ();
if (!empty ($feeds)) {
?><li><?php
$c_active = false;
if ($this->get_c == $cat->id ()) {
$c_active = true;
}
?><div class="category stick<?php echo $c_active ? ' active' : ''; ?>"><?php
?><a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id(); echo Minz_Url::display($arUrl); ?>"><?php echo $cat->name (); ?></a><?php
?><a class="btn dropdown-toggle" href="#"><?php echo FreshRSS_Themes::icon($c_active ? 'up' : 'down'); ?></a><?php
?></div><?php
?><ul class="feeds<?php echo $c_active ? ' active' : ''; ?>"><?php
foreach ($feeds as $feed) {
$feed_id = $feed->id ();
$nbEntries = $feed->nbEntries ();
$f_active = ($this->get_f == $feed_id);
?><li id="f_<?php echo $feed_id; ?>" class="item<?php echo $f_active ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>"><?php
?><div class="dropdown"><?php
?><div class="dropdown-target"></div><?php
?><a class="dropdown-toggle" data-fweb="<?php echo $feed->website (); ?>"><?php echo FreshRSS_Themes::icon('configure'); ?></a><?php
/* feed_config_template */
?></div><?php
?> <img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <?php
?><a class="feed" data-unread="<?php echo formatNumber($feed->nbNotRead()); ?>" data-priority="<?php echo $feed->priority (); ?>" href="<?php $arUrl['params']['get'] = 'f_' . $feed_id; echo Minz_Url::display($arUrl); ?>"><?php echo $feed->name(); ?></a><?php
?></li><?php
}
?></ul><?php
?></li><?php
}
} ?>
</ul>
<span class="aside_flux_ender"><!-- For fixed menu --></span>
</div>
<script id="feed_config_template" type="text/html">
<ul class="dropdown-menu">
<li class="dropdown-close"><a href="#close"></a></li>
<li class="item"><a href="<?php echo _url ('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo Minz_Translate::t ('filter'); ?></a></li>
<li class="item"><a target="_blank" href="http://example.net/"><?php echo Minz_Translate::t ('see_website'); ?></a></li>
<?php if ($this->loginOk) { ?>
<li class="separator"></li>
<li class="item"><a href="<?php echo _url ('configure', 'feed', 'id', '!!!!!!'); ?>"><?php echo Minz_Translate::t ('administration'); ?></a></li>
<li class="item"><a href="<?php echo _url ('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo Minz_Translate::t ('actualize'); ?></a></li>
<li class="item"><a href="<?php echo _url ('entry', 'read', 'get', 'f_!!!!!!'); ?>"><?php echo Minz_Translate::t ('mark_read'); ?></a></li>
<?php } ?>
</ul>
</script>

View file

@ -0,0 +1,9 @@
<ul class="nav nav-list aside">
<li class="nav-header"><?php echo Minz_Translate::t ('stats'); ?></li>
<li class="item<?php echo Minz_Request::actionName () == 'index' ? ' active' : ''; ?>">
<a href="<?php echo _url ('stats', 'index'); ?>"><?php echo Minz_Translate::t ('stats_main'); ?></a>
</li>
<li class="item<?php echo Minz_Request::actionName () == 'idle' ? ' active' : ''; ?>">
<a href="<?php echo _url ('stats', 'idle'); ?>"><?php echo Minz_Translate::t ('stats_idle'); ?></a>
</li>
</ul>

109
sources/app/layout/header.phtml Executable file
View file

@ -0,0 +1,109 @@
<?php
if (Minz_Configuration::canLogIn()) {
?><ul class="nav nav-head nav-login"><?php
switch (Minz_Configuration::authType()) {
case 'form':
if ($this->loginOk) {
?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _t('logout'); ?></a></li><?php
} else {
?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
}
break;
case 'persona':
if ($this->loginOk) {
?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="#"><?php echo _t('logout'); ?></a></li><?php
} else {
?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
}
break;
}
?></ul><?php
}
?>
<div class="header">
<div class="item title">
<h1>
<a href="<?php echo _url('index', 'index'); ?>">
<img class="logo" src="<?php echo _i('icon', true); ?>" alt="⊚" />
<?php echo Minz_Configuration::title(); ?>
</a>
</h1>
</div>
<div class="item search">
<?php if ($this->loginOk || Minz_Configuration::allowAnonymous()) { ?>
<form action="<?php echo _url('index', 'index'); ?>" method="get">
<div class="stick">
<?php $search = Minz_Request::param('search', ''); ?>
<input type="search" name="search" id="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo _t('search'); ?>" />
<?php $get = Minz_Request::param('get', ''); ?>
<?php if ($get != '') { ?>
<input type="hidden" name="get" value="<?php echo $get; ?>" />
<?php } ?>
<?php $order = Minz_Request::param('order', ''); ?>
<?php if ($order != '') { ?>
<input type="hidden" name="order" value="<?php echo $order; ?>" />
<?php } ?>
<?php $state = Minz_Request::param('state', ''); ?>
<?php if ($state != '') { ?>
<input type="hidden" name="state" value="<?php echo $state; ?>" />
<?php } ?>
<button class="btn" type="submit"><?php echo _i('search'); ?></button>
</div>
</form>
<?php } ?>
</div>
<?php if ($this->loginOk) { ?>
<div class="item configure">
<div class="dropdown">
<div id="dropdown-configure" class="dropdown-target"></div>
<a class="btn dropdown-toggle" href="#dropdown-configure"><?php echo _i('configure'); ?></a>
<ul class="dropdown-menu">
<li class="dropdown-close"><a href="#close"></a></li>
<li class="dropdown-header"><?php echo _t('configuration'); ?></li>
<li class="item"><a href="<?php echo _url('configure', 'display'); ?>"><?php echo _t('display_configuration'); ?></a></li>
<li class="item"><a href="<?php echo _url('configure', 'reading'); ?>"><?php echo _t('reading_configuration'); ?></a></li>
<li class="item"><a href="<?php echo _url('configure', 'archiving'); ?>"><?php echo _t('archiving_configuration'); ?></a></li>
<li class="item"><a href="<?php echo _url('configure', 'sharing'); ?>"><?php echo _t('sharing'); ?></a></li>
<li class="item"><a href="<?php echo _url('configure', 'shortcut'); ?>"><?php echo _t('shortcuts'); ?></a></li>
<li class="item"><a href="<?php echo _url('configure', 'queries'); ?>"><?php echo _t('queries'); ?></a></li>
<li class="separator"></li>
<li class="item"><a href="<?php echo _url('configure', 'users'); ?>"><?php echo _t('users'); ?></a></li>
<li class="separator"></li>
<li class="item"><a href="<?php echo _url('stats', 'index'); ?>"><?php echo _t('stats'); ?></a></li>
<li class="item"><a href="<?php echo _url('index', 'logs'); ?>"><?php echo _t('logs'); ?></a></li>
<li class="item"><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about'); ?></a></li>
<?php
if (Minz_Configuration::canLogIn()) {
?><li class="separator"></li><?php
switch (Minz_Configuration::authType()) {
case 'form':
?><li class="item"><a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
break;
case 'persona':
?><li class="item"><a class="signout" href="#"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
break;
}
} ?>
</ul>
</div>
</div>
<?php } elseif (Minz_Configuration::canLogIn()) {
?><div class="item configure"><?php
switch (Minz_Configuration::authType()) {
case 'form':
echo _i('login'); ?><a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
break;
case 'persona':
echo _i('login'); ?><a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
break;
}
?></div><?php
} ?>
</div>

61
sources/app/layout/layout.phtml Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="<?php echo $this->conf->language; ?>" xml:lang="<?php echo $this->conf->language; ?>">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1.0" />
<?php echo self::headTitle(); ?>
<?php echo self::headStyle(); ?>
<?php echo self::headScript(); ?>
<script>//<![CDATA[
<?php $this->renderHelper('javascript_vars'); ?>
//]]></script>
<?php
if (!empty($this->nextId)) {
$params = Minz_Request::params();
$params['next'] = $this->nextId;
?>
<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display(array('c' => Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), 'params' => $params)); ?>" />
<?php } ?>
<link rel="shortcut icon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" />
<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" />
<?php
if (isset($this->url)) {
$rss_url = $this->url;
$rss_url['params']['output'] = 'rss';
?>
<link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($rss_url); ?>" />
<?php } ?>
<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('starred', true); ?>">
<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('non-starred', true); ?>">
<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('read', true); ?>">
<link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('unread', true); ?>">
<link rel="apple-touch-icon" href="<?php echo Minz_Url::display('/themes/icons/apple-touch-icon.png'); ?>">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="<?php echo Minz_Configuration::title(); ?>">
<meta name="msapplication-TileColor" content="#FFF" />
<meta name="robots" content="noindex,nofollow" />
</head>
<body class="<?php echo Minz_Request::param('output', 'normal'); ?>">
<?php $this->partial('header'); ?>
<div id="global">
<?php $this->render(); ?>
</div>
<?php
$msg = '';
$status = 'closed';
if (isset($this->notification)) {
$msg = $this->notification['content'];
$status = $this->notification['type'];
invalidateHttpCache();
}
?>
<div id="notification" class="notification <?php echo $status; ?>">
<span class="msg"><?php echo $msg; ?></span>
<a class="close" href=""><?php echo FreshRSS_Themes::icon('close'); ?></a>
</div>
</body>
</html>

View file

@ -0,0 +1,5 @@
<ul id="nav_entries">
<li class="item"><a class="previous_entry" href="#"><?php echo FreshRSS_Themes::icon('prev'); ?></a></li>
<li class="item"><a class="up" href="#"><?php echo FreshRSS_Themes::icon('up'); ?></a></li>
<li class="item"><a class="next_entry" href="#"><?php echo FreshRSS_Themes::icon('next'); ?></a></li>
</ul>

273
sources/app/layout/nav_menu.phtml Executable file
View file

@ -0,0 +1,273 @@
<?php
$actual_view = Minz_Request::param('output', 'normal');
?>
<div class="nav_menu">
<?php if ($actual_view === 'normal') { ?>
<a class="btn toggle_aside" href="#aside_flux"><?php echo FreshRSS_Themes::icon('category'); ?></a>
<?php } ?>
<?php if ($this->loginOk) { ?>
<div class="stick">
<?php
$url_state = $this->url;
if ($this->state & FreshRSS_Entry::STATE_READ) {
$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_READ;
$checked = 'true';
$class = 'active';
} else {
$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_READ;
$checked = 'false';
$class = '';
}
?>
<a id="toggle-read"
class="btn <?php echo $class; ?>"
aria-checked="<?php echo $checked; ?>"
href="<?php echo Minz_Url::display ($url_state); ?>"
title="<?php echo Minz_Translate::t ('show_read'); ?>">
<?php echo FreshRSS_Themes::icon('read'); ?>
</a>
<?php
if ($this->state & FreshRSS_Entry::STATE_NOT_READ) {
$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_READ;
$checked = 'true';
$class = 'active';
} else {
$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_READ;
$checked = 'false';
$class = '';
}
?>
<a id="toggle-unread"
class="btn <?php echo $class; ?>"
aria-checked="<?php echo $checked; ?>"
href="<?php echo Minz_Url::display ($url_state); ?>"
title="<?php echo Minz_Translate::t ('show_not_reads'); ?>">
<?php echo FreshRSS_Themes::icon('unread'); ?>
</a>
<?php
if ($this->state & FreshRSS_Entry::STATE_FAVORITE) {
$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_FAVORITE;
$checked = 'true';
$class = 'active';
} else {
$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_FAVORITE;
$checked = 'false';
$class = '';
}
?>
<a id="toggle-favorite"
class="btn <?php echo $class; ?>"
aria-checked="<?php echo $checked; ?>"
href="<?php echo Minz_Url::display ($url_state); ?>"
title="<?php echo Minz_Translate::t ('show_favorite'); ?>">
<?php echo FreshRSS_Themes::icon('starred'); ?>
</a>
<?php
if ($this->state & FreshRSS_Entry::STATE_NOT_FAVORITE) {
$url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_FAVORITE;
$checked = 'true';
$class = 'active';
} else {
$url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_FAVORITE;
$checked = 'false';
$class = '';
}
?>
<a id="toggle-not-favorite"
class="btn <?php echo $class; ?>"
aria-checked="<?php echo $checked; ?>"
href="<?php echo Minz_Url::display ($url_state); ?>"
title="<?php echo Minz_Translate::t ('show_not_favorite'); ?>">
<?php echo FreshRSS_Themes::icon('non-starred'); ?>
</a>
<div class="dropdown">
<div id="dropdown-query" class="dropdown-target"></div>
<a class="dropdown-toggle btn" href="#dropdown-query"><?php echo FreshRSS_Themes::icon('down'); ?></a>
<ul class="dropdown-menu">
<li class="dropdown-close"><a href="#close"></a></li>
<li class="dropdown-header"><?php echo Minz_Translate::t('queries'); ?> <a class="no-mobile" href="<?php echo _url('configure', 'queries'); ?>"><?php echo FreshRSS_Themes::icon('configure'); ?></a></li>
<?php foreach ($this->conf->queries as $query) { ?>
<li class="item">
<a href="<?php echo $query['url']; ?>"><?php echo $query['name']; ?></a>
</li>
<?php } ?>
<?php if (count($this->conf->queries) > 0) { ?>
<li class="separator no-mobile"></li>
<?php } ?>
<?php
$url_query = $this->url;
$url_query['c'] = 'configure';
$url_query['a'] = 'addQuery';
?>
<li class="item no-mobile"><a href="<?php echo Minz_Url::display($url_query); ?>"><?php echo FreshRSS_Themes::icon('bookmark-add'); ?> <?php echo Minz_Translate::t('add_query'); ?></a></li>
</ul>
</div>
</div>
<?php
$get = false;
$string_mark = Minz_Translate::t ('mark_all_read');
if ($this->get_f) {
$get = 'f_' . $this->get_f;
$string_mark = Minz_Translate::t ('mark_feed_read');
} elseif ($this->get_c && $this->get_c != 'a') {
if ($this->get_c === 's') {
$get = 's';
} else {
$get = 'c_' . $this->get_c;
}
$string_mark = Minz_Translate::t ('mark_cat_read');
}
$nextGet = $get;
if ($this->conf->onread_jump_next && (strlen ($get) > 2)) {
$anotherUnreadId = '';
$foundCurrent = false;
switch ($get[0]) {
case 'c':
foreach ($this->cat_aside as $cat) {
if ($cat->id () == $this->get_c) {
$foundCurrent = true;
continue;
}
if ($cat->nbNotRead () <= 0) continue;
$anotherUnreadId = $cat->id ();
if ($foundCurrent) break;
}
$nextGet = empty ($anotherUnreadId) ? 'a' : 'c_' . $anotherUnreadId;
break;
case 'f':
foreach ($this->cat_aside as $cat) {
if ($cat->id () == $this->get_c) {
foreach ($cat->feeds () as $feed) {
if ($feed->id () == $this->get_f) {
$foundCurrent = true;
continue;
}
if ($feed->nbNotRead () <= 0) continue;
$anotherUnreadId = $feed->id ();
if ($foundCurrent) break;
}
break;
}
}
$nextGet = empty ($anotherUnreadId) ? 'c_' . $this->get_c : 'f_' . $anotherUnreadId;
break;
}
}
if ($this->order === 'ASC') {
$idMax = 0;
} else {
$p = isset($this->entries[0]) ? $this->entries[0] : null;
$idMax = $p === null ? '0' : $p->id();
}
$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('get' => $get, 'nextGet' => $nextGet, 'idMax' => $idMax));
$output = Minz_Request::param('output', '');
if (($output != '') && ($this->conf->view_mode !== $output)) {
$arUrl['params']['output'] = $output;
}
$markReadUrl = Minz_Url::display($arUrl);
Minz_Session::_param ('markReadUrl', $markReadUrl);
?>
<div class="stick" id="nav_menu_read_all">
<a class="read_all btn<?php if ($this->conf->reading_confirm) {echo ' confirm';} ?>" href="<?php echo $markReadUrl; ?>"><?php echo Minz_Translate::t ('mark_read'); ?></a>
<div class="dropdown">
<div id="dropdown-read" class="dropdown-target"></div>
<a class="dropdown-toggle btn" href="#dropdown-read"><?php echo FreshRSS_Themes::icon('down'); ?></a>
<ul class="dropdown-menu">
<li class="dropdown-close"><a href="#close"></a></li>
<li class="item"><a href="<?php echo $markReadUrl; ?>"><?php echo $string_mark; ?></a></li>
<li class="separator"></li>
<?php
$today = $this->today;
$one_week = $today - 604800;
?>
<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'idMax', $today . '000000'); ?>"><?php echo Minz_Translate::t ('before_one_day'); ?></a></li>
<li class="item"><a href="<?php echo _url ('entry', 'read', 'is_read', 1, 'get', $get, 'idMax', $one_week . '000000'); ?>"><?php echo Minz_Translate::t ('before_one_week'); ?></a></li>
</ul>
</div>
</div>
<?php } ?>
<?php $url_output = $this->url; ?>
<div class="stick" id="nav_menu_views">
<?php $url_output['params']['output'] = 'normal'; ?>
<a class="view_normal btn <?php echo $actual_view == 'normal'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('normal_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
<?php echo FreshRSS_Themes::icon("view-normal"); ?>
</a>
<?php $url_output['params']['output'] = 'global'; ?>
<a class="view_global btn <?php echo $actual_view == 'global'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('global_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
<?php echo FreshRSS_Themes::icon("view-global"); ?>
</a>
<?php $url_output['params']['output'] = 'reader'; ?>
<a class="view_reader btn <?php echo $actual_view == 'reader'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('reader_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
<?php echo FreshRSS_Themes::icon("view-reader"); ?>
</a>
<?php
$url_output['params']['output'] = 'rss';
$url_output['params']['token'] = $this->conf->token;
?>
<a class="view_rss btn" target="_blank" title="<?php echo Minz_Translate::t ('rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
<?php echo FreshRSS_Themes::icon('rss'); ?>
</a>
</div>
<div class="item search">
<form action="<?php echo _url ('index', 'index'); ?>" method="get">
<?php $search = Minz_Request::param ('search', ''); ?>
<input type="search" name="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo Minz_Translate::t ('search_short'); ?>" />
<?php $get = Minz_Request::param ('get', ''); ?>
<?php if($get != '') { ?>
<input type="hidden" name="get" value="<?php echo $get; ?>" />
<?php } ?>
<?php $order = Minz_Request::param ('order', ''); ?>
<?php if($order != '') { ?>
<input type="hidden" name="order" value="<?php echo $order; ?>" />
<?php } ?>
<?php $state = Minz_Request::param ('state', ''); ?>
<?php if($state != '') { ?>
<input type="hidden" name="state" value="<?php echo $state; ?>" />
<?php } ?>
</form>
</div>
<?php
if ($this->order === 'DESC') {
$order = 'ASC';
$icon = 'up';
$title = 'older_first';
} else {
$order = 'DESC';
$icon = 'down';
$title = 'newer_first';
}
$url_order = $this->url;
$url_order['params']['order'] = $order;
?>
<a class="btn" href="<?php echo Minz_Url::display ($url_order); ?>" title="<?php echo Minz_Translate::t ($title); ?>">
<?php echo FreshRSS_Themes::icon($icon); ?>
</a>
<?php if ($this->loginOk || Minz_Configuration::allowAnonymousRefresh()) { ?>
<a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><?php echo FreshRSS_Themes::icon('refresh'); ?></a>
<?php } ?>
</div>

View file

@ -0,0 +1,79 @@
<?php $this->partial('aside_configure'); ?>
<div class="post">
<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url('configure', 'archiving'); ?>">
<legend><?php echo Minz_Translate::t('archiving_configuration'); ?></legend>
<p><?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('archiving_configuration_help'); ?></p>
<div class="form-group">
<label class="group-name" for="old_entries"><?php echo Minz_Translate::t('delete_articles_every'); ?></label>
<div class="group-controls">
<input type="number" id="old_entries" name="old_entries" min="1" max="1200" value="<?php echo $this->conf->old_entries; ?>" /> <?php echo Minz_Translate::t('month'); ?>
  <a class="btn confirm" href="<?php echo _url('entry', 'purge'); ?>"><?php echo Minz_Translate::t('purge_now'); ?></a>
</div>
</div>
<div class="form-group">
<label class="group-name" for="keep_history_default"><?php echo Minz_Translate::t('keep_history'), ' ', Minz_Translate::t('by_feed'); ?></label>
<div class="group-controls">
<select class="number" name="keep_history_default" id="keep_history_default" required="required"><?php
foreach (array('' => '', 0 => '0', 10 => '10', 50 => '50', 100 => '100', 500 => '500', 1000 => '1 000', 5000 => '5 000', 10000 => '10 000', -1 => '∞') as $v => $t) {
echo '<option value="' . $v . ($this->conf->keep_history_default == $v ? '" selected="selected' : '') . '">' . $t . ' </option>';
}
?></select> (<?php echo Minz_Translate::t('by_default'); ?>)
</div>
</div>
<div class="form-group">
<label class="group-name" for="ttl_default"><?php echo Minz_Translate::t('ttl'); ?></label>
<div class="group-controls">
<select class="number" name="ttl_default" id="ttl_default" required="required"><?php
$found = false;
foreach (array(1200 => '20min', 1500 => '25min', 1800 => '30min', 2700 => '45min',
3600 => '1h', 5400 => '1.5h', 7200 => '2h', 10800 => '3h', 14400 => '4h', 18800 => '5h', 21600 => '6h', 25200 => '7h', 28800 => '8h',
36000 => '10h', 43200 => '12h', 64800 => '18h',
86400 => '1d', 129600 => '1.5d', 172800 => '2d', 259200 => '3d', 345600 => '4d', 432000 => '5d', 518400 => '6d',
604800 => '1wk', -1 => '∞') as $v => $t) {
echo '<option value="' . $v . ($this->conf->ttl_default == $v ? '" selected="selected' : '') . '">' . $t . '</option>';
if ($this->conf->ttl_default == $v) {
$found = true;
}
}
if (!$found) {
echo '<option value="' . intval($this->conf->ttl_default) . '" selected="selected">' . intval($this->conf->ttl_default) . 's</option>';
}
?></select> (<?php echo Minz_Translate::t('by_default'); ?>)
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
</div>
</div>
</form>
<form method="post" action="<?php echo _url('entry', 'optimize'); ?>">
<legend><?php echo Minz_Translate::t ('advanced'); ?></legend>
<div class="form-group">
<p class="group-name"><?php echo Minz_Translate::t('current_user'); ?></p>
<div class="group-controls">
<p><?php echo formatNumber($this->nb_total), ' ', Minz_Translate::t('articles'), ', ', formatBytes($this->size_user); ?></p>
<input type="hidden" name="optimiseDatabase" value="1" />
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('optimize_bdd'); ?></button>
<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('optimize_todo_sometimes'); ?>
</div>
</div>
<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
<div class="form-group">
<p class="group-name"><?php echo Minz_Translate::t('users'); ?></p>
<div class="group-controls">
<p><?php echo formatBytes($this->size_total); ?></p>
</div>
</div>
<?php } ?>
</form>
</div>

View file

@ -0,0 +1,49 @@
<?php $this->partial ('aside_feed'); ?>
<div class="post">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url ('configure', 'categorize'); ?>">
<legend><?php echo Minz_Translate::t ('categories_management'); ?></legend>
<p class="alert alert-warn"><?php echo Minz_Translate::t ('feeds_moved_category_deleted', $this->defaultCategory->name ()); ?></p>
<?php $i = 0; foreach ($this->categories as $cat) { $i++; ?>
<div class="form-group">
<label class="group-name" for="cat_<?php echo $cat->id (); ?>">
<?php echo Minz_Translate::t ('category_number', $i); ?>
</label>
<div class="group-controls">
<div class="stick">
<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
<?php if ($cat->nbFeed () > 0) { ?>
<button type="submit" class="btn btn-attention confirm" formaction="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></button>
<?php } ?>
</div>
(<?php echo Minz_Translate::t ('number_feeds', $cat->nbFeed ()); ?>)
<?php if ($cat->id () === $this->defaultCategory->id ()) { ?>
<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('can_not_be_deleted'); ?>
<?php } ?>
<input type="hidden" name="ids[]" value="<?php echo $cat->id (); ?>" />
</div>
</div>
<?php } ?>
<div class="form-group">
<label class="group-name" for="new_category"><?php echo Minz_Translate::t ('add_category'); ?></label>
<div class="group-controls">
<input type="text" id="new_category" name="new_category" placeholder="<?php echo Minz_Translate::t ('new_category'); ?>" />
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,102 @@
<?php $this->partial ('aside_configure'); ?>
<div class="post">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url ('configure', 'display'); ?>">
<legend><?php echo Minz_Translate::t ('display_configuration'); ?></legend>
<div class="form-group">
<label class="group-name" for="language"><?php echo Minz_Translate::t ('language'); ?></label>
<div class="group-controls">
<select name="language" id="language">
<?php $languages = $this->conf->availableLanguages (); ?>
<?php foreach ($languages as $short => $lib) { ?>
<option value="<?php echo $short; ?>"<?php echo $this->conf->language === $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label class="group-name" for="theme"><?php echo Minz_Translate::t ('theme'); ?></label>
<div class="group-controls">
<select name="theme" id="theme" required=""><?php
$found = false;
foreach ($this->themes as $theme) {
?><option value="<?php echo $theme['id']; ?>"<?php if ($this->conf->theme === $theme['id']) { echo ' selected="selected"'; $found = true; } ?>><?php
echo $theme['name'] . ' — ' . Minz_Translate::t ('by') . ' ' . $theme['author'];
?></option><?php
}
if (!$found) {
?><option selected="selected"></option><?php
}
?></select>
</div>
</div>
<?php $width = $this->conf->content_width; ?>
<div class="form-group">
<label class="group-name" for="content_width"><?php echo Minz_Translate::t('content_width'); ?></label>
<div class="group-controls">
<select name="content_width" id="content_width" required="">
<option value="thin" <?php echo $width === 'thin'? 'selected="selected"' : ''; ?>>
<?php echo Minz_Translate::t('width_thin'); ?>
</option>
<option value="medium" <?php echo $width === 'medium'? 'selected="selected"' : ''; ?>>
<?php echo Minz_Translate::t('width_medium'); ?>
</option>
<option value="large" <?php echo $width === 'large'? 'selected="selected"' : ''; ?>>
<?php echo Minz_Translate::t('width_large'); ?>
</option>
<option value="no_limit" <?php echo $width === 'no_limit'? 'selected="selected"' : ''; ?>>
<?php echo Minz_Translate::t('width_no_limit'); ?>
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="group-name" for="theme"><?php echo Minz_Translate::t ('article_icons'); ?></label>
<table>
<thead>
<tr>
<th> </th>
<th title="<?php echo Minz_Translate::t ('mark_read'); ?>"><?php echo FreshRSS_Themes::icon('read'); ?></th>
<th title="<?php echo Minz_Translate::t ('mark_favorite'); ?>"><?php echo FreshRSS_Themes::icon('bookmark'); ?></th>
<th><?php echo Minz_Translate::t ('sharing'); ?></th>
<th><?php echo Minz_Translate::t ('related_tags'); ?></th>
<th><?php echo Minz_Translate::t ('publication_date'); ?></th>
<th><?php echo FreshRSS_Themes::icon('link'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<th><?php echo Minz_Translate::t ('top_line'); ?></th>
<td><input type="checkbox" name="topline_read" value="1"<?php echo $this->conf->topline_read ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" name="topline_favorite" value="1"<?php echo $this->conf->topline_favorite ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" disabled="disabled" /></td>
<td><input type="checkbox" disabled="disabled" /></td>
<td><input type="checkbox" name="topline_date" value="1"<?php echo $this->conf->topline_date ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" name="topline_link" value="1"<?php echo $this->conf->topline_link ? ' checked="checked"' : ''; ?> /></td>
</tr><tr>
<th><?php echo Minz_Translate::t ('bottom_line'); ?></th>
<td><input type="checkbox" name="bottomline_read" value="1"<?php echo $this->conf->bottomline_read ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" name="bottomline_favorite" value="1"<?php echo $this->conf->bottomline_favorite ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" name="bottomline_sharing" value="1"<?php echo $this->conf->bottomline_sharing ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" name="bottomline_tags" value="1"<?php echo $this->conf->bottomline_tags ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" name="bottomline_date" value="1"<?php echo $this->conf->bottomline_date ? ' checked="checked"' : ''; ?> /></td>
<td><input type="checkbox" name="bottomline_link" value="1"<?php echo $this->conf->bottomline_link ? ' checked="checked"' : ''; ?> /></td>
</tr>
</tbody>
</table><br />
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,176 @@
<?php $this->partial ('aside_feed'); ?>
<?php if ($this->flux) { ?>
<div class="post">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> <?php echo Minz_Translate::t ('or'); ?> <a href="<?php echo _url ('index', 'index', 'get', 'f_' . $this->flux->id ()); ?>"><?php echo Minz_Translate::t ('filter'); ?></a>
<h1><?php echo $this->flux->name (); ?></h1>
<?php echo $this->flux->description (); ?>
<?php $nbEntries = $this->flux->nbEntries (); ?>
<?php if ($this->flux->inError ()) { ?>
<p class="alert alert-error"><span class="alert-head"><?php echo Minz_Translate::t ('damn'); ?></span> <?php echo Minz_Translate::t ('feed_in_error'); ?></p>
<?php } elseif ($nbEntries === 0) { ?>
<p class="alert alert-warn"><?php echo Minz_Translate::t ('feed_empty'); ?></p>
<?php } ?>
<form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>" autocomplete="off">
<legend><?php echo Minz_Translate::t ('informations'); ?></legend>
<div class="form-group">
<label class="group-name" for="name"><?php echo Minz_Translate::t ('title'); ?></label>
<div class="group-controls">
<input type="text" name="name" id="name" class="extend" value="<?php echo $this->flux->name () ; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="description"><?php echo Minz_Translate::t ('feed_description'); ?></label>
<div class="group-controls">
<textarea name="description" id="description"><?php echo htmlspecialchars($this->flux->description(), ENT_NOQUOTES, 'UTF-8'); ?></textarea>
</div>
</div>
<div class="form-group">
<label class="group-name" for="website"><?php echo Minz_Translate::t ('website_url'); ?></label>
<div class="group-controls">
<div class="stick">
<input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" />
<a class="btn" target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
</div>
</div>
</div>
<div class="form-group">
<label class="group-name" for="url"><?php echo Minz_Translate::t ('feed_url'); ?></label>
<div class="group-controls">
<div class="stick">
<input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" />
<a class="btn" target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
</div>
<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a>
</div>
</div>
<div class="form-group">
<label class="group-name" for="category"><?php echo Minz_Translate::t ('category'); ?></label>
<div class="group-controls">
<select name="category" id="category">
<?php foreach ($this->categories as $cat) { ?>
<option value="<?php echo $cat->id (); ?>"<?php echo $cat->id ()== $this->flux->category () ? ' selected="selected"' : ''; ?>>
<?php echo $cat->name (); ?>
</option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label class="group-name" for="priority"><?php echo Minz_Translate::t ('show_in_all_flux'); ?></label>
<div class="group-controls">
<label class="checkbox" for="priority">
<input type="checkbox" name="priority" id="priority" value="10"<?php echo $this->flux->priority () > 0 ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('yes'); ?>
</label>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button class="btn btn-attention confirm" formmethod="post" formaction="<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'delete', 'params' => array ('id' => $this->flux->id ()))); ?>"><?php echo Minz_Translate::t ('delete'); ?></button>
</div>
</div>
<legend><?php echo Minz_Translate::t ('archiving_configuration'); ?></legend>
<div class="form-group">
<label class="group-name"></label>
<div class="group-controls">
<a class="btn" href="<?php echo _url ('feed', 'actualize', 'id', $this->flux->id ()); ?>">
<?php echo FreshRSS_Themes::icon('refresh'); ?> <?php echo Minz_Translate::t('actualize'); ?>
</a>
</div>
</div>
<div class="form-group">
<label class="group-name"><?php echo Minz_Translate::t ('number_articles'); ?></label>
<div class="group-controls">
<span class="control"><?php echo $nbEntries; ?></span>
</div>
</div>
<div class="form-group">
<label class="group-name" for="keep_history"><?php echo Minz_Translate::t ('keep_history'); ?></label>
<div class="group-controls">
<select class="number" name="keep_history" id="keep_history" required="required"><?php
foreach (array('' => '', -2 => Minz_Translate::t('by_default'), 0 => '0', 10 => '10', 50 => '50', 100 => '100', 500 => '500', 1000 => '1 000', 5000 => '5 000', 10000 => '10 000', -1 => '∞') as $v => $t) {
echo '<option value="' . $v . ($this->flux->keepHistory() === $v ? '" selected="selected' : '') . '">' . $t . '</option>';
}
?></select>
</div>
</div>
<div class="form-group">
<label class="group-name" for="ttl"><?php echo Minz_Translate::t('ttl'); ?></label>
<div class="group-controls">
<select class="number" name="ttl" id="ttl" required="required"><?php
$found = false;
foreach (array(-2 => Minz_Translate::t('by_default'), 900 => '15min', 1200 => '20min', 1500 => '25min', 1800 => '30min', 2700 => '45min',
3600 => '1h', 5400 => '1.5h', 7200 => '2h', 10800 => '3h', 14400 => '4h', 18800 => '5h', 21600 => '6h', 25200 => '7h', 28800 => '8h',
36000 => '10h', 43200 => '12h', 64800 => '18h',
86400 => '1d', 129600 => '1.5d', 172800 => '2d', 259200 => '3d', 345600 => '4d', 432000 => '5d', 518400 => '6d',
604800 => '1wk', 1209600 => '2wk', 1814400 => '3wk', 2419200 => '4wk', 2629744 => '1mo', -1 => '∞') as $v => $t) {
echo '<option value="' . $v . ($this->flux->ttl() === $v ? '" selected="selected' : '') . '">' . $t . '</option>';
if ($this->flux->ttl() == $v) {
$found = true;
}
}
if (!$found) {
echo '<option value="' . intval($this->flux->ttl()) . '" selected="selected">' . intval($this->flux->ttl()) . 's</option>';
}
?></select>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button class="btn btn-attention confirm" formmethod="post" formaction="<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'truncate', 'params' => array ('id' => $this->flux->id ()))); ?>"><?php echo Minz_Translate::t ('truncate'); ?></button>
</div>
</div>
<legend><?php echo Minz_Translate::t ('login_configuration'); ?></legend>
<?php $auth = $this->flux->httpAuth (false); ?>
<div class="form-group">
<label class="group-name" for="http_user"><?php echo Minz_Translate::t ('http_username'); ?></label>
<div class="group-controls">
<input type="text" name="http_user" id="http_user" class="extend" value="<?php echo $auth['username']; ?>" autocomplete="off" />
<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('access_protected_feeds'); ?>
</div>
<label class="group-name" for="http_pass"><?php echo Minz_Translate::t ('http_password'); ?></label>
<div class="group-controls">
<input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
</div>
</div>
<legend><?php echo Minz_Translate::t ('advanced'); ?></legend>
<div class="form-group">
<label class="group-name" for="path_entries"><?php echo Minz_Translate::t ('css_path_on_website'); ?></label>
<div class="group-controls">
<input type="text" name="path_entries" id="path_entries" class="extend" value="<?php echo $this->flux->pathEntries (); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" />
<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('retrieve_truncated_feeds'); ?>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
</div>
</div>
</form>
</div>
<?php } else { ?>
<div class="alert alert-warn"><span class="alert-head"><?php echo Minz_Translate::t ('no_selected_feed'); ?></span> <?php echo Minz_Translate::t ('think_to_add'); ?></div>
<?php } ?>

View file

@ -0,0 +1,90 @@
<?php $this->partial('aside_configure'); ?>
<div class="post">
<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url('configure', 'queries'); ?>">
<legend><?php echo _t('queries'); ?></legend>
<?php foreach ($this->conf->queries as $key => $query) { ?>
<div class="form-group" id="query-group-<?php echo $key; ?>">
<label class="group-name" for="queries_<?php echo $key; ?>_name">
<?php echo _t('query_number', $key + 1); ?>
</label>
<div class="group-controls">
<input type="hidden" id="queries_<?php echo $key; ?>_search" name="queries[<?php echo $key; ?>][search]" value="<?php echo isset($query['search']) ? $query['search'] : ""; ?>"/>
<input type="hidden" id="queries_<?php echo $key; ?>_state" name="queries[<?php echo $key; ?>][state]" value="<?php echo isset($query['state']) ? $query['state'] : ""; ?>"/>
<input type="hidden" id="queries_<?php echo $key; ?>_order" name="queries[<?php echo $key; ?>][order]" value="<?php echo isset($query['order']) ? $query['order'] : ""; ?>"/>
<input type="hidden" id="queries_<?php echo $key; ?>_get" name="queries[<?php echo $key; ?>][get]" value="<?php echo isset($query['get']) ? $query['get'] : ""; ?>"/>
<div class="stick">
<input class="extend"
type="text"
id="queries_<?php echo $key; ?>_name"
name="queries[<?php echo $key; ?>][name]"
value="<?php echo $query['name']; ?>"
/>
<a class="btn" href="<?php echo $query['url']; ?>">
<?php echo _i('link'); ?>
</a>
<a class="btn btn-attention remove" href="#" data-remove="query-group-<?php echo $key; ?>">
<?php echo _i('close'); ?>
</a>
</div>
<?php
$exist = (isset($query['search']) ? 1 : 0)
+ (isset($query['state']) ? 1 : 0)
+ (isset($query['order']) ? 1 : 0)
+ (isset($query['get']) ? 1 : 0);
// If the only filter is "all" articles, we consider there is no filter
$exist = ($exist === 1 && isset($query['get']) && $query['get'] === 'a') ? 0 : $exist;
?>
<?php if ($exist === 0) { ?>
<div class="alert alert-warn">
<div class="alert-head"><?php echo _t('no_query_filter'); ?></div>
</div>
<?php } else { ?>
<div class="alert alert-success">
<div class="alert-head"><?php echo _t('query_filter'); ?></div>
<ul>
<?php if (isset($query['search'])) { $exist = true; ?>
<li class="item"><?php echo _t('query_search', $query['search']); ?></li>
<?php } ?>
<?php if (isset($query['state'])) { $exist = true; ?>
<li class="item"><?php echo _t('query_state_' . $query['state']); ?></li>
<?php } ?>
<?php if (isset($query['order'])) { $exist = true; ?>
<li class="item"><?php echo _t('query_order_' . strtolower($query['order'])); ?></li>
<?php } ?>
<?php if (isset($query['get'])) { $exist = true; ?>
<li class="item"><?php echo _t('query_get_' . $this->query_get[$key]['type'], $this->query_get[$key]['name']); ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
</div>
</div>
<?php } ?>
<?php if (count($this->conf->queries) > 0) { ?>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo _t('save'); ?></button>
<button type="reset" class="btn"><?php echo _t('cancel'); ?></button>
</div>
</div>
<?php } else { ?>
<p class="alert alert-warn"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('no_query'); ?></p>
<?php } ?>
</form>
</div>

View file

@ -0,0 +1,135 @@
<?php $this->partial ('aside_configure'); ?>
<div class="post">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url ('configure', 'reading'); ?>">
<legend><?php echo Minz_Translate::t ('reading_configuration'); ?></legend>
<div class="form-group">
<label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('articles_per_page'); ?></label>
<div class="group-controls">
<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="sort_order"><?php echo Minz_Translate::t ('sort_order'); ?></label>
<div class="group-controls">
<select name="sort_order" id="sort_order">
<option value="DESC"<?php echo $this->conf->sort_order === 'DESC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('newer_first'); ?></option>
<option value="ASC"<?php echo $this->conf->sort_order === 'ASC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('older_first'); ?></option>
</select>
</div>
</div>
<div class="form-group">
<label class="group-name" for="view_mode"><?php echo Minz_Translate::t ('default_view'); ?></label>
<div class="group-controls">
<select name="view_mode" id="view_mode">
<option value="normal"<?php echo $this->conf->view_mode === 'normal' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('normal_view'); ?></option>
<option value="reader"<?php echo $this->conf->view_mode === 'reader' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('reader_view'); ?></option>
<option value="global"<?php echo $this->conf->view_mode === 'global' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('global_view'); ?></option>
</select>
<label class="radio" for="radio_all">
<input type="radio" name="default_view" id="radio_all" value="<?php echo FreshRSS_Entry::STATE_ALL; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_ALL ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('show_all_articles'); ?>
</label>
<label class="radio" for="radio_not_read">
<input type="radio" name="default_view" id="radio_not_read" value="<?php echo FreshRSS_Entry::STATE_NOT_READ; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_NOT_READ ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('show_not_reads'); ?>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="auto_load_more">
<input type="checkbox" name="auto_load_more" id="auto_load_more" value="1"<?php echo $this->conf->auto_load_more ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('auto_load_more'); ?>
<noscript><strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="display_posts">
<input type="checkbox" name="display_posts" id="display_posts" value="1"<?php echo $this->conf->display_posts ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('display_articles_unfolded'); ?>
<noscript><strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="lazyload">
<input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('img_with_lazyload'); ?>
<noscript><strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="sticky_post">
<input type="checkbox" name="sticky_post" id="sticky_post" value="1"<?php echo $this->conf->sticky_post ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('sticky_post'); ?>
<noscript><strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="reading_confirm">
<input type="checkbox" name="reading_confirm" id="reading_confirm" value="1"<?php echo $this->conf->reading_confirm ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('reading_confirm'); ?>
<noscript><strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
</label>
</div>
</div>
<div class="form-group">
<label class="group-name"><?php echo Minz_Translate::t ('auto_read_when'); ?></label>
<div class="group-controls">
<label class="checkbox" for="check_open_article">
<input type="checkbox" name="mark_open_article" id="check_open_article" value="1"<?php echo $this->conf->mark_when['article'] ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t('article_viewed'); ?>
</label>
<label class="checkbox" for="check_open_site">
<input type="checkbox" name="mark_open_site" id="check_open_site" value="1"<?php echo $this->conf->mark_when['site'] ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('article_open_on_website'); ?>
</label>
<label class="checkbox" for="check_scroll">
<input type="checkbox" name="mark_scroll" id="check_scroll" value="1"<?php echo $this->conf->mark_when['scroll'] ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('scroll'); ?>
</label>
<label class="checkbox" for="check_reception">
<input type="checkbox" name="mark_upon_reception" id="check_reception" value="1"<?php echo $this->conf->mark_when['reception'] ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('upon_reception'); ?>
</label>
</div>
</div>
<div class="form-group">
<label class="group-name"><?php echo Minz_Translate::t ('after_onread'); ?></label>
<div class="group-controls">
<label class="checkbox" for="onread_jump_next">
<input type="checkbox" name="onread_jump_next" id="onread_jump_next" value="1"<?php echo $this->conf->onread_jump_next ? ' checked="checked"' : ''; ?> />
<?php echo Minz_Translate::t ('jump_next'); ?>
</label>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,59 @@
<?php $this->partial ('aside_configure'); ?>
<div class="post">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url ('configure', 'sharing'); ?>"
data-simple='<div class="form-group" id="group-share-##key##"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo FreshRSS_Themes::icon('close'); ?></a>
<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /></div></div>'
data-advanced='<div class="form-group" id="group-share-##key##"><label class="group-name">##label##</label><div class="group-controls">
<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
<div class="stick">
<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo FreshRSS_Themes::icon('close'); ?></a></div>
<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="##help##"><?php echo FreshRSS_Themes::icon('help'); ?></a>
</div></div>'>
<legend><?php echo Minz_Translate::t ('sharing'); ?></legend>
<?php foreach ($this->conf->sharing as $key => $sharing): ?>
<?php $share = $this->conf->shares[$sharing['type']]; ?>
<div class="form-group" id="group-share-<?php echo $key; ?>">
<label class="group-name">
<?php echo Minz_Translate::t ($sharing['type']); ?>
</label>
<div class="group-controls">
<input type='hidden' id='share_<?php echo $key;?>_type' name="share[<?php echo $key;?>][type]" value='<?php echo $sharing['type']?>' />
<?php if ($share['form'] === 'advanced') { ?>
<div class="stick">
<input type="text" id="share_<?php echo $key;?>_name" name="share[<?php echo $key;?>][name]" class="extend" value="<?php echo $sharing['name']?>" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
<input type="url" id="share_<?php echo $key;?>_url" name="share[<?php echo $key;?>][url]" class="extend" value="<?php echo $sharing['url']?>" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo FreshRSS_Themes::icon('close'); ?></a>
</div>
<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="<?php echo $share['help']?>"><?php echo FreshRSS_Themes::icon('help'); ?></a>
<?php } else { ?>
<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo FreshRSS_Themes::icon('close'); ?></a>
<?php } ?>
</div>
</div>
<?php endforeach;?>
<div class="form-group">
<div class="group-controls">
<select>
<?php foreach($this->conf->shares as $key => $params):?>
<option value='<?php echo $key?>' data-form='<?php echo $params['form']?>' data-help='<?php if (!empty($params['help'])) {echo $params['help'];}?>'><?php echo Minz_Translate::t($key) ?></option>
<?php endforeach; ?>
</select>
<a href='#' class='share add btn'><?php echo FreshRSS_Themes::icon('add'); ?></a>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,113 @@
<?php $this->partial ('aside_configure'); ?>
<div class="post">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<datalist id="keys">
<?php foreach ($this->list_keys as $key) { ?>
<option value="<?php echo $key; ?>">
<?php } ?>
</datalist>
<?php $s = $this->conf->shortcuts; ?>
<form method="post" action="<?php echo _url ('configure', 'shortcut'); ?>">
<legend><?php echo Minz_Translate::t ('shortcuts'); ?></legend>
<noscript><p class="alert alert-error"><?php echo Minz_Translate::t ('javascript_for_shortcuts'); ?></p></noscript>
<legend><?php echo Minz_Translate::t ('shortcuts_navigation'); ?></legend>
<div class="form-group">
<label class="group-name" for="next_entry"><?php echo Minz_Translate::t ('next_article'); ?></label>
<div class="group-controls">
<input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="prev_entry"><?php echo Minz_Translate::t ('previous_article'); ?></label>
<div class="group-controls">
<input type="text" id="prev_entry" name="shortcuts[prev_entry]" list="keys" value="<?php echo $s['prev_entry']; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="first_entry"><?php echo Minz_Translate::t ('first_article'); ?></label>
<div class="group-controls">
<input type="text" id="first_entry" name="shortcuts[first_entry]" list="keys" value="<?php echo $s['first_entry']; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="last_entry"><?php echo Minz_Translate::t ('last_article'); ?></label>
<div class="group-controls">
<input type="text" id="last_entry" name="shortcuts[last_entry]" list="keys" value="<?php echo $s['last_entry']; ?>" />
</div>
</div>
<div><?php echo Minz_Translate::t ('shortcuts_navigation_help');?></div>
<legend><?php echo Minz_Translate::t ('shortcuts_article_action');?></legend>
<div class="form-group">
<label class="group-name" for="mark_read"><?php echo Minz_Translate::t ('mark_read'); ?></label>
<div class="group-controls">
<input type="text" id="mark_read" name="shortcuts[mark_read]" list="keys" value="<?php echo $s['mark_read']; ?>" />
<?php echo Minz_Translate::t ('shift_for_all_read'); ?>
</div>
</div>
<div class="form-group">
<label class="group-name" for="mark_favorite"><?php echo Minz_Translate::t ('mark_favorite'); ?></label>
<div class="group-controls">
<input type="text" id="mark_favorite" name="shortcuts[mark_favorite]" list="keys" value="<?php echo $s['mark_favorite']; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="go_website"><?php echo Minz_Translate::t ('see_on_website'); ?></label>
<div class="group-controls">
<input type="text" id="go_website" name="shortcuts[go_website]" list="keys" value="<?php echo $s['go_website']; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="auto_share_shortcut"><?php echo Minz_Translate::t ('auto_share'); ?></label>
<div class="group-controls">
<input type="text" id="auto_share_shortcut" name="shortcuts[auto_share]" list="keys" value="<?php echo $s['auto_share']; ?>" />
<?php echo Minz_Translate::t ('auto_share_help'); ?>
</div>
</div>
<div class="form-group">
<label class="group-name" for="collapse_entry"><?php echo Minz_Translate::t ('collapse_article'); ?></label>
<div class="group-controls">
<input type="text" id="collapse_entry" name="shortcuts[collapse_entry]" list="keys" value="<?php echo $s['collapse_entry']; ?>" />
</div>
</div>
<legend><?php echo Minz_Translate::t ('shortcuts_other_action');?></legend>
<div class="form-group">
<label class="group-name" for="load_more_shortcut"><?php echo Minz_Translate::t ('load_more'); ?></label>
<div class="group-controls">
<input type="text" id="load_more_shortcut" name="shortcuts[load_more]" list="keys" value="<?php echo $s['load_more']; ?>" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="focus_search_shortcut"><?php echo Minz_Translate::t ('focus_search'); ?></label>
<div class="group-controls">
<input type="text" id="focus_search_shortcut" name="shortcuts[focus_search]" list="keys" value="<?php echo $s['focus_search']; ?>" />
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,211 @@
<?php $this->partial('aside_configure'); ?>
<div class="post">
<a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url('users', 'auth'); ?>">
<legend><?php echo Minz_Translate::t('login_configuration'); ?></legend>
<div class="form-group">
<label class="group-name" for="current_user"><?php echo Minz_Translate::t('current_user'); ?></label>
<div class="group-controls">
<input id="current_user" type="text" disabled="disabled" value="<?php echo Minz_Session::param('currentUser', '_'); ?>" />
<label class="checkbox" for="is_admin">
<input type="checkbox" id="is_admin" disabled="disabled" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? 'checked="checked" ' : ''; ?>/>
<?php echo Minz_Translate::t('is_admin'); ?>
</label>
</div>
</div>
<div class="form-group">
<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
<div class="group-controls">
<div class="stick">
<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
</div>
<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
</div>
</div>
<?php if (Minz_Configuration::apiEnabled()) { ?>
<div class="form-group">
<label class="group-name" for="apiPasswordPlain"><?php echo Minz_Translate::t('password_api'); ?></label>
<div class="group-controls">
<div class="stick">
<input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
</div>
</div>
</div>
<?php } ?>
<div class="form-group">
<label class="group-name" for="mail_login"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
<?php $mail = $this->conf->mail_login; ?>
<div class="group-controls">
<input type="email" id="mail_login" name="mail_login" class="extend" autocomplete="off" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" />
<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
</div>
</div>
<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
<legend><?php echo Minz_Translate::t('auth_type'); ?></legend>
<div class="form-group">
<label class="group-name" for="auth_type"><?php echo Minz_Translate::t('auth_type'); ?></label>
<div class="group-controls">
<select id="auth_type" name="auth_type" required="required">
<?php if (!in_array(Minz_Configuration::authType(), array('form', 'persona', 'http_auth', 'none'))) { ?>
<option selected="selected"></option>
<?php } ?>
<option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : '', cryptAvailable() ? '' : ' disabled="disabled"'; ?>><?php echo Minz_Translate::t('auth_form'); ?></option>
<option value="persona"<?php echo Minz_Configuration::authType() === 'persona' ? ' selected="selected"' : '', $this->conf->mail_login == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('auth_persona'); ?></option>
<option value="http_auth"<?php echo Minz_Configuration::authType() === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('http_auth'); ?> (REMOTE_USER = '<?php echo httpAuthUser(); ?>')</option>
<option value="none"<?php echo Minz_Configuration::authType() === 'none' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t('auth_none'); ?></option>
</select>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="anon_access">
<input type="checkbox" name="anon_access" id="anon_access" value="1"<?php echo Minz_Configuration::allowAnonymous() ? ' checked="checked"' : '',
Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
<?php echo Minz_Translate::t('allow_anonymous', Minz_Configuration::defaultUser()); ?>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="anon_refresh">
<input type="checkbox" name="anon_refresh" id="anon_refresh" value="1"<?php echo Minz_Configuration::allowAnonymousRefresh() ? ' checked="checked"' : '',
Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
<?php echo Minz_Translate::t('allow_anonymous_refresh'); ?>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="unsafe_autologin">
<input type="checkbox" name="unsafe_autologin" id="unsafe_autologin" value="1"<?php echo Minz_Configuration::unsafeAutologinEnabled() ? ' checked="checked"' : '',
Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
<?php echo Minz_Translate::t('unsafe_autologin'); ?>
<kbd>p/i/?a=formLogin&amp;u=Alice&amp;p=1234</kbd>
</label>
</div>
</div>
<?php if (Minz_Configuration::canLogIn()) { ?>
<div class="form-group">
<label class="group-name" for="token"><?php echo Minz_Translate::t('auth_token'); ?></label>
<?php $token = $this->conf->token; ?>
<div class="group-controls">
<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php
echo Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> />
<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('explain_token', Minz_Url::display(null, 'html', true), $token); ?>
</div>
</div>
<?php } ?>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="api_enabled">
<input type="checkbox" name="api_enabled" id="api_enabled" value="1"<?php echo Minz_Configuration::apiEnabled() ? ' checked="checked"' : '',
Minz_Configuration::needsLogin() ? '' : ' disabled="disabled"'; ?> />
<?php echo Minz_Translate::t('api_enabled'); ?>
</label>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
</div>
</div>
</form>
<form method="post" action="<?php echo _url('users', 'delete'); ?>">
<legend><?php echo Minz_Translate::t('users'); ?></legend>
<div class="form-group">
<label class="group-name" for="users_list"><?php echo Minz_Translate::t('users_list'); ?></label>
<div class="group-controls">
<select id="users_list" name="username"><?php
foreach (listUsers() as $user) {
echo '<option>', $user, '</option>';
}
?></select>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-attention confirm"><?php echo Minz_Translate::t('delete'); ?></button>
</div>
</div>
</form>
<form method="post" action="<?php echo _url('users', 'create'); ?>">
<legend><?php echo Minz_Translate::t('create_user'); ?></legend>
<div class="form-group">
<label class="group-name" for="new_user_language"><?php echo Minz_Translate::t ('language'); ?></label>
<div class="group-controls">
<select name="new_user_language" id="new_user_language">
<?php $languages = $this->conf->availableLanguages (); ?>
<?php foreach ($languages as $short => $lib) { ?>
<option value="<?php echo $short; ?>"<?php echo $this->conf->language === $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group">
<label class="group-name" for="new_user_name"><?php echo Minz_Translate::t('username'); ?></label>
<div class="group-controls">
<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" autocomplete="off" pattern="[0-9a-zA-Z]{1,16}" placeholder="demo" />
</div>
</div>
<div class="form-group">
<label class="group-name" for="new_user_passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
<div class="group-controls">
<div class="stick">
<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
<a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a>
</div>
<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
</div>
</div>
<div class="form-group">
<label class="group-name" for="new_user_email"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
<?php $mail = $this->conf->mail_login; ?>
<div class="group-controls">
<input type="email" id="new_user_email" name="new_user_email" class="extend" autocomplete="off" placeholder="alice@example.net" />
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('create'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
</div>
</div>
</form>
<?php } ?>
</div>

View file

@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
if (Minz_Request::param ('is_favorite', true)) {
Minz_Request::_param ('is_favorite', 0);
} else {
Minz_Request::_param ('is_favorite', 1);
}
$url = Minz_Url::display (array (
'c' => Minz_Request::controllerName (),
'a' => Minz_Request::actionName (),
'params' => Minz_Request::params (),
));
echo json_encode (array ('url' => str_ireplace ('&amp;', '&', $url), 'icon' => FreshRSS_Themes::icon(Minz_Request::param ('is_favorite') ? 'non-starred' : 'starred')));

View file

@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
if (Minz_Request::param ('is_read', true)) {
Minz_Request::_param ('is_read', 0);
} else {
Minz_Request::_param ('is_read', 1);
}
$url = Minz_Url::display (array (
'c' => Minz_Request::controllerName (),
'a' => Minz_Request::actionName (),
'params' => Minz_Request::params (),
));
echo json_encode (array ('url' => str_ireplace ('&amp;', '&', $url), 'icon' => FreshRSS_Themes::icon(Minz_Request::param ('is_read') ? 'unread' : 'read')));

View file

@ -0,0 +1,18 @@
<div class="post">
<div class="alert alert-error">
<h1 class="alert-head"><?php echo $this->code; ?></h1>
<p>
<?php
switch(Minz_Request::param ('code')) {
case 403:
echo Minz_Translate::t ('forbidden_access');
break;
case 404:
default:
echo Minz_Translate::t ('page_not_found');
} ?><br />
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
</p>
</div>
</div>

View file

@ -0,0 +1 @@
OK

View file

@ -0,0 +1,91 @@
<?php if ($this->feed) { ?>
<div class="post">
<h1><?php echo Minz_Translate::t ('add_rss_feed'); ?></h1>
<?php if (!$this->load_ok) { ?>
<p class="alert alert-error"><span class="alert-head"><?php echo Minz_Translate::t('damn'); ?></span> <?php echo Minz_Translate::t('internal_problem_feed', _url('index', 'logs')); ?></p>
<?php } ?>
<form method="post" action="<?php echo _url('feed', 'add'); ?>" autocomplete="off">
<legend><?php echo Minz_Translate::t('informations'); ?></legend>
<?php if ($this->load_ok) { ?>
<div class="form-group">
<label class="group-name"><?php echo Minz_Translate::t('title'); ?></label>
<div class="group-controls">
<label><?php echo $this->feed->name() ; ?></label>
</div>
</div>
<?php $desc = $this->feed->description(); if ($desc != '') { ?>
<div class="form-group">
<label class="group-name"><?php echo Minz_Translate::t('feed_description'); ?></label>
<div class="group-controls">
<label><?php echo htmlspecialchars($desc, ENT_NOQUOTES, 'UTF-8'); ?></label>
</div>
</div>
<?php } ?>
<div class="form-group">
<label class="group-name"><?php echo Minz_Translate::t('website_url'); ?></label>
<div class="group-controls">
<?php echo $this->feed->website(); ?>
<a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
</div>
</div>
<?php } ?>
<div class="form-group">
<label class="group-name" for="url"><?php echo Minz_Translate::t('feed_url'); ?></label>
<div class="group-controls">
<div class="stick">
<input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" />
<a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
</div>
<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo Minz_Translate::t('feed_validator'); ?></a>
</div>
</div>
<div class="form-group">
<label class="group-name" for="category"><?php echo Minz_Translate::t('category'); ?></label>
<div class="group-controls">
<select name="category" id="category">
<?php foreach ($this->categories as $cat) { ?>
<option value="<?php echo $cat->id(); ?>"<?php echo $cat->id() == 1 ? ' selected="selected"' : ''; ?>>
<?php echo $cat->name(); ?>
</option>
<?php } ?>
<option value="nc"><?php echo Minz_Translate::t('new_category'); ?></option>
</select>
<span style="display: none;">
<input type="text" name="new_category[name]" id="new_category_name" autocomplete="off" placeholder="<?php echo Minz_Translate::t('new_category'); ?>" />
</span>
</div>
</div>
<legend><?php echo Minz_Translate::t('http_authentication'); ?></legend>
<?php $auth = $this->feed->httpAuth(false); ?>
<div class="form-group">
<label class="group-name" for="http_user"><?php echo Minz_Translate::t('http_username'); ?></label>
<div class="group-controls">
<input type="text" name="http_user" id="http_user" class="extend" value="<?php echo $auth['username']; ?>" autocomplete="off" />
</div>
<label class="group-name" for="http_pass"><?php echo Minz_Translate::t('http_password'); ?></label>
<div class="group-controls">
<input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
</div>
<div class="group-controls">
<?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('access_protected_feeds'); ?>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button>
<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
</div>
</div>
</form>
</div>
<?php } ?>

View file

@ -0,0 +1,47 @@
<?php
$username = Minz_Session::param('currentUser', '_');
$articles = array(
'id' => 'user/' . str_replace('/', '', $username) . '/state/org.freshrss/' . $this->type,
'title' => $this->list_title,
'author' => $username,
'items' => array()
);
foreach ($this->entries as $entry) {
if (!isset($this->feed)) {
$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed ());
} else {
$feed = $this->feed;
}
$articles['items'][] = array(
'id' => $entry->guid(),
'categories' => array_values($entry->tags()),
'title' => $entry->title(),
'author' => $entry->author(),
'published' => $entry->date(true),
'updated' => $entry->date(true),
'alternate' => array(array(
'href' => $entry->link(),
'type' => 'text/html'
)),
'content' => array(
'content' => $entry->content()
),
'origin' => array(
'streamId' => $feed->id(),
'title' => $feed->name(),
'htmlUrl' => $feed->website(),
'feedUrl' => $feed->url()
)
);
}
$options = 0;
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
$options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
}
echo json_encode($articles, $options);
?>

View file

@ -0,0 +1,28 @@
<?php
$opml_array = array(
'head' => array(
'title' => Minz_Configuration::title(),
'dateCreated' => date('D, d M Y H:i:s')
),
'body' => array()
);
foreach ($this->categories as $key => $cat) {
$opml_array['body'][$key] = array(
'text' => $cat['name'],
'@outlines' => array()
);
foreach ($cat['feeds'] as $feed) {
$opml_array['body'][$key]['@outlines'][] = array(
'text' => htmlspecialchars_decode($feed->name()),
'type' => 'rss',
'xmlUrl' => $feed->url(),
'htmlUrl' => $feed->website(),
'description' => $feed->description()
);
}
}
echo libopml_render($opml_array);

View file

@ -0,0 +1,55 @@
<?php
echo '"use strict";', "\n";
$mark = $this->conf->mark_when;
echo 'var ',
'hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true',
',display_order="', Minz_Request::param('order', $this->conf->sort_order), '"',
',auto_mark_article=', $mark['article'] ? 'true' : 'false',
',auto_mark_site=', $mark['site'] ? 'true' : 'false',
',auto_mark_scroll=', $mark['scroll'] ? 'true' : 'false',
',auto_load_more=', $this->conf->auto_load_more ? 'true' : 'false',
',full_lazyload=', $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'true' : 'false',
',does_lazyload=', $this->conf->lazyload ? 'true' : 'false',
',sticky_post=', $this->conf->sticky_post ? 'true' : 'false';
$s = $this->conf->shortcuts;
echo ',shortcuts={',
'mark_read:"', $s['mark_read'], '",',
'mark_favorite:"', $s['mark_favorite'], '",',
'go_website:"', $s['go_website'], '",',
'prev_entry:"', $s['prev_entry'], '",',
'next_entry:"', $s['next_entry'], '",',
'first_entry:"', $s['first_entry'], '",',
'last_entry:"', $s['last_entry'], '",',
'collapse_entry:"', $s['collapse_entry'], '",',
'load_more:"', $s['load_more'], '",',
'auto_share:"', $s['auto_share'], '",',
'focus_search:"', $s['focus_search'], '"',
"},\n";
if (Minz_Request::param ('output') === 'global') {
echo "iconClose='", FreshRSS_Themes::icon('close'), "',\n";
}
$authType = Minz_Configuration::authType();
if ($authType === 'persona') {
// If user is disconnected, current_user_mail MUST be null
$mail = Minz_Session::param ('mail', false);
if ($mail) {
echo 'current_user_mail="' . $mail . '",';
} else {
echo 'current_user_mail=null,';
}
}
echo 'authType="', $authType, '",',
'url_freshrss="', _url ('index', 'index'), '",',
'url_login="', _url ('index', 'login'), '",',
'url_logout="', _url ('index', 'logout'), '",';
echo 'str_confirmation="', Minz_Translate::t('confirm_action'), '"', ",\n";
$autoActualise = Minz_Session::param('actualize_feeds', false);
echo 'auto_actualize_feeds=', $autoActualise ? 'true' : 'false', ";\n";

View file

@ -0,0 +1,47 @@
<?php
$c = Minz_Request::controllerName ();
$a = Minz_Request::actionName ();
$params = Minz_Request::params ();
?>
<?php if ($this->nbPage > 1) { ?>
<ul class="pagination">
<?php $params[$getteur] = 1; ?>
<li class="item pager-first">
<?php if ($this->currentPage > 1) { ?>
<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">« <?php echo Minz_Translate::t('first'); ?></a>
<?php } ?>
</li>
<?php $params[$getteur] = $this->currentPage - 1; ?>
<li class="item pager-previous">
<?php if ($this->currentPage > 1) { ?>
<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"> <?php echo Minz_Translate::t('previous'); ?></a>
<?php } ?>
</li>
<?php for ($i = $this->currentPage - 2; $i <= $this->currentPage + 2; $i++) { ?>
<?php if($i > 0 && $i <= $this->nbPage) { ?>
<?php if ($i != $this->currentPage) { ?>
<?php $params[$getteur] = $i; ?>
<li class="item pager-item"><a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo $i; ?></a></li>
<?php } else { ?>
<li class="item pager-current"><?php echo $i; ?></li>
<?php } ?>
<?php } ?>
<?php } ?>
<?php $params[$getteur] = $this->currentPage + 1; ?>
<li class="item pager-next">
<?php if ($this->currentPage < $this->nbPage) { ?>
<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Minz_Translate::t('next'); ?> </a>
<?php } ?>
</li>
<?php $params[$getteur] = $this->nbPage; ?>
<li class="item pager-last">
<?php if ($this->currentPage < $this->nbPage) { ?>
<a href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Minz_Translate::t('last'); ?> »</a>
<?php } ?>
</li>
</ul>
<?php } ?>

View file

@ -0,0 +1,26 @@
<?php
$c = Minz_Request::controllerName ();
$a = Minz_Request::actionName ();
$params = Minz_Request::params ();
$markReadUrl = Minz_Session::param ('markReadUrl');
Minz_Session::_param ('markReadUrl', false);
?>
<ul class="pagination">
<li class="item pager-next">
<?php if (!empty($this->nextId)) { ?>
<?php $params['next'] = $this->nextId; ?>
<a id="load_more" href="<?php echo Minz_Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Minz_Translate::t ('load_more'); ?></a>
<?php } elseif ($markReadUrl) { ?>
<a id="bigMarkAsRead" href="<?php echo $markReadUrl; ?>"<?php if ($this->conf->reading_confirm) { echo ' class="confirm"';} ?>>
<?php echo Minz_Translate::t ('nothing_to_load'); ?><br />
<span class="bigTick"></span><br />
<?php echo Minz_Translate::t ('mark_all_read'); ?>
</a>
<?php } else { ?>
<a id="bigMarkAsRead" href=".">
<?php echo Minz_Translate::t ('nothing_to_load'); ?><br />
</a>
<?php } ?>
</li>
</ul>

View file

@ -0,0 +1,45 @@
<?php $this->partial ('nav_menu'); ?>
<div id="stream" class="global categories">
<?php
$arUrl = array('c' => 'index', 'a' => 'index', 'params' => array());
if ($this->conf->view_mode !== 'normal') {
$arUrl['params']['output'] = 'normal';
}
$p = Minz_Request::param('state', '');
if (($p != '') && ($this->conf->default_view !== $p)) {
$arUrl['params']['state'] = $p;
}
foreach ($this->cat_aside as $cat) {
$feeds = $cat->feeds ();
if (!empty ($feeds)) {
?>
<div class="box-category">
<div class="category">
<a data-unread="<?php echo formatNumber($cat->nbNotRead()); ?>" class="btn" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id (); echo Minz_Url::display($arUrl); ?>">
<?php echo $cat->name(); ?>
</a>
</div>
<ul class="feeds">
<?php foreach ($feeds as $feed) { ?>
<?php $not_read = $feed->nbNotRead (); ?>
<li id="f_<?php echo $feed->id (); ?>" class="item<?php echo $feed->inError () ? ' error' : ''; ?><?php echo $feed->nbEntries () == 0 ? ' empty' : ''; ?>">
<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" />
<a class="feed" data-unread="<?php echo formatNumber($feed->nbNotRead()); ?>" data-priority="<?php echo $feed->priority (); ?>" href="<?php $arUrl['params']['get'] = 'f_' . $feed->id(); echo Minz_Url::display($arUrl); ?>">
<?php echo $feed->name(); ?>
</a>
</li>
<?php } ?>
</ul>
</div>
<?php
}
}
?>
</div>
<div id="overlay"></div>
<div id="panel"<?php echo $this->conf->display_posts ? '' : ' class="hide_posts"'; ?>>
<a class="close" href="#"><?php echo FreshRSS_Themes::icon('close'); ?></a>
</div>

View file

@ -0,0 +1,189 @@
<?php
$this->partial ('aside_flux');
$this->partial ('nav_menu');
if (!empty($this->entries)) {
$display_today = true;
$display_yesterday = true;
$display_others = true;
if ($this->loginOk) {
$sharing = $this->conf->sharing;
} else {
$sharing = array();
}
$hidePosts = !$this->conf->display_posts;
$lazyload = $this->conf->lazyload;
$topline_read = $this->conf->topline_read;
$topline_favorite = $this->conf->topline_favorite;
$topline_date = $this->conf->topline_date;
$topline_link = $this->conf->topline_link;
$bottomline_read = $this->conf->bottomline_read;
$bottomline_favorite = $this->conf->bottomline_favorite;
$bottomline_sharing = $this->conf->bottomline_sharing && (count($sharing));
$bottomline_tags = $this->conf->bottomline_tags;
$bottomline_date = $this->conf->bottomline_date;
$bottomline_link = $this->conf->bottomline_link;
$content_width = $this->conf->content_width;
?>
<div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>"><?php
?><div id="new-article">
<a href="<?php echo Minz_Url::display ($this->url); ?>"><?php echo Minz_Translate::t ('new_article'); ?></a>
</div><?php
foreach ($this->entries as $item) {
if ($display_today && $item->isDay (FreshRSS_Days::TODAY, $this->today)) {
?><div class="day" id="day_today"><?php
echo Minz_Translate::t ('today');
?><span class="date"><?php echo timestamptodate (time (), false); ?></span><?php
?><span class="name"><?php echo $this->currentName; ?></span><?php
?></div><?php
$display_today = false;
}
if ($display_yesterday && $item->isDay (FreshRSS_Days::YESTERDAY, $this->today)) {
?><div class="day" id="day_yesterday"><?php
echo Minz_Translate::t ('yesterday');
?><span class="date"><?php echo timestamptodate (time () - 86400, false); ?></span><?php
?><span class="name"><?php echo $this->currentName; ?></span><?php
?></div><?php
$display_yesterday = false;
}
if ($display_others && $item->isDay (FreshRSS_Days::BEFORE_YESTERDAY, $this->today)) {
?><div class="day" id="day_before_yesterday"><?php
echo Minz_Translate::t ('before_yesterday');
?><span class="name"><?php echo $this->currentName; ?></span><?php
?></div><?php
$display_others = false;
}
?><div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
<ul class="horizontal-list flux_header"><?php
if ($this->loginOk) {
if ($topline_read) {
?><li class="item manage"><?php
$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id ()));
if ($item->isRead()) {
$arUrl['params']['is_read'] = 0;
}
?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
echo FreshRSS_Themes::icon($item->isRead () ? 'read' : 'unread'); ?></a><?php
?></li><?php
}
if ($topline_favorite) {
?><li class="item manage"><?php
$arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id ()));
if ($item->isFavorite()) {
$arUrl['params']['is_favorite'] = 0;
}
?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
echo FreshRSS_Themes::icon($item->isFavorite () ? 'starred' : 'non-starred'); ?></a><?php
?></li><?php
}
}
$feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed ()); //We most likely already have the feed object in cache
if (empty($feed)) $feed = $item->feed (true);
?><li class="item website"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span></a></li>
<li class="item title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></li>
<?php if ($topline_date) { ?><li class="item date"><?php echo $item->date (); ?> </li><?php } ?>
<?php if ($topline_link) { ?><li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a></li><?php } ?>
</ul>
<div class="flux_content">
<div class="content <?php echo $content_width; ?>">
<h1 class="title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></h1>
<?php
$author = $item->author ();
echo $author != '' ? '<div class="author">' . Minz_Translate::t ('by_author', $author) . '</div>' : '';
if ($lazyload) {
echo $hidePosts ? lazyIframe(lazyimg($item->content())) : lazyimg($item->content());
} else {
echo $item->content();
}
?>
</div>
<ul class="horizontal-list bottom"><?php
if ($this->loginOk) {
if ($bottomline_read) {
?><li class="item manage"><?php
$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id ()));
if ($item->isRead()) {
$arUrl['params']['is_read'] = 0;
}
?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
echo FreshRSS_Themes::icon($item->isRead () ? 'read' : 'unread'); ?></a><?php
?></li><?php
}
if ($bottomline_favorite) {
?><li class="item manage"><?php
$arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id ()));
if ($item->isFavorite()) {
$arUrl['params']['is_favorite'] = 0;
}
?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php
echo FreshRSS_Themes::icon($item->isFavorite () ? 'starred' : 'non-starred'); ?></a><?php
?></li><?php
}
} ?>
<li class="item"><?php
if ($bottomline_sharing) {
$link = urlencode ($item->link ());
$title = urlencode ($item->title () . ' · ' . $feed->name ());
?><div class="dropdown">
<div id="dropdown-share-<?php echo $item->id ();?>" class="dropdown-target"></div>
<a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id ();?>">
<?php echo FreshRSS_Themes::icon('share'); ?>
<?php echo Minz_Translate::t ('share'); ?>
</a>
<ul class="dropdown-menu">
<li class="dropdown-close"><a href="#close"></a></li>
<?php foreach ($sharing as $share) :?>
<li class="item share">
<a target="_blank" href="<?php echo FreshRSS_Share::generateUrl($this->conf->shares, $share, $item->link(), $item->title() . ' . ' . $feed->name())?>">
<?php echo Minz_Translate::t ($share['name']);?>
</a>
</li>
<?php endforeach;?>
</ul>
</div>
<?php } ?>
</li><?php
$tags = $bottomline_tags ? $item->tags() : null;
if (!empty($tags)) {
?><li class="item">
<div class="dropdown">
<div id="dropdown-tags-<?php echo $item->id ();?>" class="dropdown-target"></div>
<a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id ();?>"><?php
echo FreshRSS_Themes::icon('tag'), Minz_Translate::t ('related_tags');
?></a>
<ul class="dropdown-menu">
<li class="dropdown-close"><a href="#close"></a></li><?php
foreach($tags as $tag) {
?><li class="item"><a href="<?php echo _url ('index', 'index', 'search', urlencode ('#' . $tag)); ?>"><?php echo $tag; ?></a></li><?php
} ?>
</ul>
</div>
</li><?php
}
if ($bottomline_date) {
?><li class="item date"><?php echo $item->date (); ?></li><?php
}
if ($bottomline_link) {
?><li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a></li><?php
} ?>
</ul>
</div>
</div>
<?php } ?>
<?php $this->renderHelper('pagination'); ?>
</div>
<?php $this->partial ('nav_entries'); ?>
<?php } else { ?>
<div id="stream" class="alert alert-warn normal">
<span class="alert-head"><?php echo Minz_Translate::t ('no_feed_to_display'); ?></span>
<?php echo Minz_Translate::t ('think_to_add'); ?>
</div>
<?php } ?>

View file

@ -0,0 +1,50 @@
<?php
$this->partial ('nav_menu');
if (!empty($this->entries)) {
$lazyload = $this->conf->lazyload;
$content_width = $this->conf->content_width;
?>
<div id="stream" class="reader">
<?php foreach ($this->entries as $item) { ?>
<div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
<div class="flux_content">
<div class="content <?php echo $content_width; ?>">
<?php
$feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed ()); //We most likely already have the feed object in cache
if (empty($feed)) $feed = $item->feed (true);
?>
<a href="<?php echo $item->link (); ?>">
<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span>
</a>
<h1 class="title"><?php echo $item->title (); ?></h1>
<div class="author">
<?php $author = $item->author (); ?>
<?php echo $author != '' ? Minz_Translate::t ('by_author', $author) . ' — ' : ''; ?>
<?php echo $item->date (); ?>
</div>
<?php
if ($lazyload) {
echo lazyimg($item->content ());
} else {
echo $item->content();
}
?>
</div>
</div>
</div>
<?php } ?>
<?php $this->renderHelper('pagination'); ?>
</div>
<?php } else { ?>
<div id="stream" class="alert alert-warn reader">
<span class="alert-head"><?php echo Minz_Translate::t ('no_feed_to_display'); ?></span>
<?php echo Minz_Translate::t ('think_to_add'); ?>
</div>
<?php } ?>

View file

@ -0,0 +1,29 @@
<?php echo '<?xml version="1.0" encoding="UTF-8" ?>'; ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title><?php echo $this->rss_title; ?></title>
<link><?php echo Minz_Url::display(null, 'html', true); ?></link>
<description><?php echo Minz_Translate::t ('rss_feeds_of', $this->rss_title); ?></description>
<pubDate><?php echo date('D, d M Y H:i:s O'); ?></pubDate>
<lastBuildDate><?php echo gmdate('D, d M Y H:i:s'); ?> GMT</lastBuildDate>
<atom:link href="<?php echo Minz_Url::display ($this->url, 'html', true); ?>" rel="self" type="application/rss+xml" />
<?php
foreach ($this->entries as $item) {
?>
<item>
<title><?php echo $item->title (); ?></title>
<link><?php echo $item->link (); ?></link>
<?php $author = $item->author (); ?>
<?php if ($author != '') { ?>
<dc:creator><?php echo $author; ?></dc:creator>
<?php } ?>
<description><![CDATA[<?php
echo $item->content ();
?>]]></description>
<pubDate><?php echo date('D, d M Y H:i:s O', $item->date (true)); ?></pubDate>
<guid isPermaLink="false"><?php echo $item->id (); ?></guid>
</item>
<?php } ?>
</channel>
</rss>

View file

@ -0,0 +1,52 @@
<?php $this->partial ('aside_feed'); ?>
<div class="post ">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<form method="post" action="<?php echo _url('importExport', 'import'); ?>" enctype="multipart/form-data">
<legend><?php echo Minz_Translate::t ('import'); ?></legend>
<div class="form-group">
<label class="group-name" for="file"><?php echo Minz_Translate::t ('file_to_import'); ?></label>
<div class="group-controls">
<input type="file" name="file" id="file" />
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('import'); ?></button>
</div>
</div>
</form>
<?php if (count($this->feeds) > 0) { ?>
<form method="post" action="<?php echo _url('importExport', 'export'); ?>">
<legend><?php echo Minz_Translate::t ('export'); ?></legend>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="export_opml">
<input type="checkbox" name="export_opml" id="export_opml" value="1" checked="checked" />
<?php echo Minz_Translate::t ('export_opml'); ?>
</label>
<label class="checkbox" for="export_starred">
<input type="checkbox" name="export_starred" id="export_starred" value="1" checked="checked" />
<?php echo Minz_Translate::t ('export_starred'); ?>
</label>
<select name="export_feeds[]" size="<?php echo min(10, count($this->feeds)); ?>" multiple="multiple">
<?php foreach ($this->feeds as $feed) { ?>
<option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('export'); ?></button>
</div>
</div>
</form>
<?php } ?>
</div>

View file

@ -0,0 +1,27 @@
<div class="post content">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<h1><?php echo Minz_Translate::t ('about_freshrss'); ?></h1>
<dl class="infos">
<dt><?php echo Minz_Translate::t ('project_website'); ?></dt>
<dd><a href="<?php echo FRESHRSS_WEBSITE; ?>"><?php echo FRESHRSS_WEBSITE; ?></a></dd>
<dt><?php echo Minz_Translate::t ('lead_developer'); ?></dt>
<dd><a href="mailto:contact@marienfressinaud.fr">Marien Fressinaud</a><a href="http://marienfressinaud.fr"><?php echo Minz_Translate::t ('website'); ?></a></dd>
<dt><?php echo Minz_Translate::t ('bugs_reports'); ?></dt>
<dd><?php echo Minz_Translate::t ('github_or_email'); ?></dd>
<dt><?php echo Minz_Translate::t ('license'); ?></dt>
<dd><?php echo Minz_Translate::t ('agpl3'); ?></dd>
<dt><?php echo Minz_Translate::t ('version'); ?></dt>
<dd><?php echo FRESHRSS_VERSION; ?></dd>
</dl>
<p><?php echo Minz_Translate::t ('freshrss_description'); ?></p>
<h1><?php echo Minz_Translate::t ('credits'); ?></h1>
<p><?php echo Minz_Translate::t ('credits_content'); ?></p>
</div>

View file

@ -0,0 +1,32 @@
<div class="prompt">
<h1><?php echo Minz_Translate::t('login'); ?></h1><?php
switch (Minz_Configuration::authType()) {
case 'form':
?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
<div>
<label for="username"><?php echo Minz_Translate::t('username'); ?></label>
<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
</div>
<div>
<label for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
<input type="password" id="passwordPlain" required="required" />
<input type="hidden" id="challenge" name="challenge" /><br />
<noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript>
</div>
<div>
<button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button>
</div>
</form><?php
break;
case 'persona':
?><p>
<?php echo FreshRSS_Themes::icon('login'); ?>
<a class="signin" href="#"><?php echo Minz_Translate::t('login_with_persona'); ?></a>
</p><?php
break;
} ?>
<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p>
</div>

View file

@ -0,0 +1,25 @@
<?php
$output = Minz_Request::param ('output', 'normal');
if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
if ($output === 'normal') {
$this->renderHelper ('view/normal_view');
} elseif ($output === 'reader') {
$this->renderHelper ('view/reader_view');
} elseif ($output === 'global') {
$this->renderHelper ('view/global_view');
} elseif ($output === 'rss') {
$this->renderHelper ('view/rss_view');
} else {
Minz_Request::_param ('output', 'normal');
$output = 'normal';
$this->renderHelper ('view/normal_view');
}
} elseif ($output === 'rss') {
// token has already been checked in the controller so we can show the view
$this->renderHelper ('view/rss_view');
} else {
// Normally, it should not happen, but log it anyway
Minz_Log::record ('Something is wrong in ' . __FILE__ . ' line ' . __LINE__, Minz_Log::ERROR);
}

View file

@ -0,0 +1 @@
<?php print_r ($this->res); ?>

View file

@ -0,0 +1 @@
OK

View file

@ -0,0 +1,25 @@
<div class="post content">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<h1><?php echo Minz_Translate::t ('logs'); ?></h1>
<form method="post" action="<?php echo _url ('index', 'logs'); ?>"><p>
<input type="hidden" name="clearLogs" />
<button type="submit" class="btn"><?php echo Minz_Translate::t ('clear_logs'); ?></button>
</p></form>
<?php $items = $this->logsPaginator->items (); ?>
<?php if (!empty ($items)) { ?>
<div class="logs">
<?php $this->logsPaginator->render ('logs_pagination.phtml', 'page'); ?>
<?php foreach ($items as $log) { ?>
<div class="log <?php echo $log->level (); ?>"><span class="date"><?php echo @date ('Y-m-d H:i:s', @strtotime ($log->date ())); ?></span><?php echo htmlspecialchars ($log->info (), ENT_NOQUOTES, 'UTF-8'); ?></div>
<?php } ?>
<?php $this->logsPaginator->render ('logs_pagination.phtml','page'); ?>
</div>
<?php } else { ?>
<p class="alert alert-warn"><?php echo Minz_Translate::t ('logs_empty'); ?></p>
<?php } ?>
</div>

View file

@ -0,0 +1,57 @@
"use strict";
var feeds = [<?php
foreach ($this->feeds as $feed) {
echo "'", Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'), "',\n";
}
?>],
feed_processed = 0,
feed_count = feeds.length;
function initProgressBar(init) {
if (init) {
$("body").after("\<div id=\"actualizeProgress\" class=\"notification good\">\
<?php echo _t('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\
<progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feed_count + "\"></progress>\
</div>");
} else {
window.location.reload();
}
}
function updateProgressBar(i) {
$("#actualizeProgressBar").val(i);
$("#actualizeProgress .progress").html(i + " / " + feed_count);
}
function updateFeeds() {
if (feed_count === 0) {
openNotification("<?php echo _t('no_feed_to_refresh'); ?>", "good");
ajax_loading = false;
return;
}
initProgressBar(true);
for (var i = 0; i < 10; i++) {
updateFeed();
}
}
function updateFeed() {
var feed = feeds.pop();
if (feed == undefined) {
return;
}
$.ajax({
type: 'POST',
url: feed,
}).complete(function (data) {
feed_processed++;
updateProgressBar(feed_processed);
if (feed_processed === feed_count) {
initProgressBar(false);
} else {
updateFeed();
}
});
}

View file

@ -0,0 +1,8 @@
<?php
$result = array();
foreach ($this->categories as $cat) {
foreach ($cat->feeds() as $feed) {
$result[$feed->id()] = $feed->nbNotRead();
}
}
echo json_encode($result);

View file

@ -0,0 +1,2 @@
<?php
echo json_encode(array('salt1' => $this->salt1, 'nonce' => $this->nonce));

View file

@ -0,0 +1,19 @@
<?php $this->partial('aside_stats'); ?>
<div class="post content">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo _t ('back_to_rss_feeds'); ?></a>
<h1><?php echo _t ('stats_idle'); ?></h1>
<?php foreach ($this->idleFeeds as $period => $feeds){ ?>
<div class="stat">
<h2><?php echo _t ($period); ?></h2>
<ul>
<?php foreach ($feeds as $feed){ ?>
<li><?php echo $feed; ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
</div>

View file

@ -0,0 +1,127 @@
<?php $this->partial('aside_stats'); ?>
<div class="post content">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo _t ('back_to_rss_feeds'); ?></a>
<h1><?php echo _t ('stats_main'); ?></h1>
<div class="stat">
<h2><?php echo _t ('stats_entry_repartition'); ?></h2>
<table>
<thead>
<tr>
<th> </th>
<th><?php echo _t ('main_stream'); ?></th>
<th><?php echo _t ('all_feeds'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<th><?php echo _t ('status_total'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['total']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['total']); ?></td>
</tr>
<tr>
<th><?php echo _t ('status_read'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['read']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['read']); ?></td>
</tr>
<tr>
<th><?php echo _t ('status_unread'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['unread']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['unread']); ?></td>
</tr>
<tr>
<th><?php echo _t ('status_favorites'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['favorite']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['favorite']); ?></td>
</tr>
</tbody>
</table>
</div>
<div class="stat">
<h2><?php echo _t ('stats_entry_per_day'); ?></h2>
<div id="statsEntryPerDay" style="height: 300px"></div>
</div>
<div class="stat">
<h2><?php echo _t ('stats_feed_per_category'); ?></h2>
<div id="statsFeedPerCategory" style="height: 300px"></div>
<div id="statsFeedPerCategoryLegend"></div>
</div>
<div class="stat">
<h2><?php echo _t ('stats_entry_per_category'); ?></h2>
<div id="statsEntryPerCategory" style="height: 300px"></div>
<div id="statsEntryPerCategoryLegend"></div>
</div>
<div class="stat">
<h2><?php echo _t ('stats_top_feed'); ?></h2>
<table>
<thead>
<tr>
<th><?php echo _t ('feed'); ?></th>
<th><?php echo _t ('category'); ?></th>
<th><?php echo _t ('stats_entry_count'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->topFeed as $feed): ?>
<tr>
<td><?php echo $feed['name']; ?></td>
<td><?php echo $feed['category']; ?></td>
<td class="numeric"><?php echo formatNumber($feed['count']); ?></td>
</tr>
<?php endforeach;?>
</tbody>
</table>
</div>
</div>
<script>
"use strict";
function initStats() {
if (!window.Flotr) {
if (window.console) {
console.log('FreshRSS waiting for Flotr…');
}
window.setTimeout(initStats, 50);
return;
}
// Entry per day
Flotr.draw(document.getElementById('statsEntryPerDay'),
[<?php echo $this->count ?>],
{
grid: {verticalLines: false},
bars: {horizontal: false, show: true},
xaxis: {noTicks: 6, showLabels: false, tickDecimals: 0},
yaxis: {min: 0},
mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
});
// Feed per category
Flotr.draw(document.getElementById('statsFeedPerCategory'),
<?php echo $this->feedByCategory ?>,
{
grid: {verticalLines: false, horizontalLines: false},
pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
xaxis: {showLabels: false},
yaxis: {showLabels: false},
mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
legend: {container: document.getElementById('statsFeedPerCategoryLegend'), noColumns: 3}
});
// Entry per category
Flotr.draw(document.getElementById('statsEntryPerCategory'),
<?php echo $this->entryByCategory ?>,
{
grid: {verticalLines: false, horizontalLines: false},
pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
xaxis: {showLabels: false},
yaxis: {showLabels: false},
mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
legend: {container: document.getElementById('statsEntryPerCategoryLegend'), noColumns: 3}
});
}
initStats();
</script>

View file

@ -0,0 +1,127 @@
<?php $this->partial('aside_stats'); ?>
<div class="post content">
<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
<h1><?php echo Minz_Translate::t ('stats_main'); ?></h1>
<div class="stat">
<h2><?php echo Minz_Translate::t ('stats_entry_repartition'); ?></h2>
<table>
<thead>
<tr>
<th> </th>
<th><?php echo Minz_Translate::t ('main_stream'); ?></th>
<th><?php echo Minz_Translate::t ('all_feeds'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<th><?php echo Minz_Translate::t ('status_total'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['total']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['total']); ?></td>
</tr>
<tr>
<th><?php echo Minz_Translate::t ('status_read'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['read']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['read']); ?></td>
</tr>
<tr>
<th><?php echo Minz_Translate::t ('status_unread'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['unread']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['unread']); ?></td>
</tr>
<tr>
<th><?php echo Minz_Translate::t ('status_favorites'); ?></th>
<td class="numeric"><?php echo formatNumber($this->repartition['main_stream']['favorite']); ?></td>
<td class="numeric"><?php echo formatNumber($this->repartition['all_feeds']['favorite']); ?></td>
</tr>
</tbody>
</table>
</div>
<div class="stat">
<h2><?php echo Minz_Translate::t ('stats_entry_per_day'); ?></h2>
<div id="statsEntryPerDay" style="height: 300px"></div>
</div>
<div class="stat">
<h2><?php echo Minz_Translate::t ('stats_feed_per_category'); ?></h2>
<div id="statsFeedPerCategory" style="height: 300px"></div>
<div id="statsFeedPerCategoryLegend"></div>
</div>
<div class="stat">
<h2><?php echo Minz_Translate::t ('stats_entry_per_category'); ?></h2>
<div id="statsEntryPerCategory" style="height: 300px"></div>
<div id="statsEntryPerCategoryLegend"></div>
</div>
<div class="stat">
<h2><?php echo Minz_Translate::t ('stats_top_feed'); ?></h2>
<table>
<thead>
<tr>
<th><?php echo Minz_Translate::t ('feed'); ?></th>
<th><?php echo Minz_Translate::t ('category'); ?></th>
<th><?php echo Minz_Translate::t ('stats_entry_count'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->topFeed as $feed): ?>
<tr>
<td><?php echo $feed['name']; ?></td>
<td><?php echo $feed['category']; ?></td>
<td class="numeric"><?php echo formatNumber($feed['count']); ?></td>
</tr>
<?php endforeach;?>
</tbody>
</table>
</div>
</div>
<script>
"use strict";
function initStats() {
if (!window.Flotr) {
if (window.console) {
console.log('FreshRSS waiting for Flotr…');
}
window.setTimeout(initStats, 50);
return;
}
// Entry per day
Flotr.draw(document.getElementById('statsEntryPerDay'),
[<?php echo $this->count ?>],
{
grid: {verticalLines: false},
bars: {horizontal: false, show: true},
xaxis: {noTicks: 6, showLabels: false, tickDecimals: 0},
yaxis: {min: 0},
mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return numberFormat(obj.y);}}
});
// Feed per category
Flotr.draw(document.getElementById('statsFeedPerCategory'),
<?php echo $this->feedByCategory ?>,
{
grid: {verticalLines: false, horizontalLines: false},
pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
xaxis: {showLabels: false},
yaxis: {showLabels: false},
mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
legend: {container: document.getElementById('statsFeedPerCategoryLegend'), noColumns: 3}
});
// Entry per category
Flotr.draw(document.getElementById('statsEntryPerCategory'),
<?php echo $this->entryByCategory ?>,
{
grid: {verticalLines: false, horizontalLines: false},
pie: {explode: 10, show: true, labelFormatter: function(){return '';}},
xaxis: {showLabels: false},
yaxis: {showLabels: false},
mouse: {relative: true, track: true, trackDecimals: 0, trackFormatter: function(obj) {return obj.series.label + ' - '+ numberFormat(obj.y) + ' ('+ (obj.fraction * 100).toFixed(1) + '%)';}},
legend: {container: document.getElementById('statsEntryPerCategoryLegend'), noColumns: 3}
});
}
initStats();
</script>

22
sources/constants.php Executable file
View file

@ -0,0 +1,22 @@
<?php
define('FRESHRSS_VERSION', '0.7.3');
define('FRESHRSS_WEBSITE', 'http://freshrss.org');
// PHP text output compression http://php.net/ob_gzhandler (better to do it at Web server level)
define('PHP_COMPRESSION', false);
// Constantes de chemins
define('FRESHRSS_PATH', dirname(__FILE__));
define('PUBLIC_PATH', FRESHRSS_PATH . '/p');
define('INDEX_PATH', PUBLIC_PATH . '/i');
define('PUBLIC_RELATIVE', '..');
define('DATA_PATH', FRESHRSS_PATH . '/data');
define('LOG_PATH', DATA_PATH . '/log');
define('CACHE_PATH', DATA_PATH . '/cache');
define('LIB_PATH', FRESHRSS_PATH . '/lib');
define('APP_PATH', FRESHRSS_PATH . '/app');
define('TMP_PATH', sys_get_temp_dir());

8
sources/data/.gitignore vendored Executable file
View file

@ -0,0 +1,8 @@
application.ini
config.php
*_user.php
*.sqlite
touch.txt
no-cache.txt
*.bak.php
*.lock.txt

3
sources/data/.htaccess Executable file
View file

@ -0,0 +1,3 @@
Order Allow,Deny
Deny from all
Satisfy all

1
sources/data/cache/.gitignore vendored Executable file
View file

@ -0,0 +1 @@
*.spc

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