Browse Source

Merge remote-tracking branch 'origin/dev' into beta

Alexandre Alapetite 12 years ago
parent
commit
4e4b0f717f
71 changed files with 2646 additions and 486 deletions
  1. 19 6
      CHANGELOG
  2. 19 2
      README.md
  3. 14 10
      app/Controllers/configureController.php
  4. 8 5
      app/Controllers/entryController.php
  5. 1 1
      app/Controllers/errorController.php
  6. 3 2
      app/Controllers/feedController.php
  7. 55 11
      app/Controllers/indexController.php
  8. 34 1
      app/Controllers/javascriptController.php
  9. 41 5
      app/Controllers/usersController.php
  10. 24 7
      app/FreshRSS.php
  11. 5 1
      app/Models/Configuration.php
  12. 1 1
      app/Models/Entry.php
  13. 3 0
      app/Models/EntryDAO.php
  14. 23 5
      app/Models/Themes.php
  15. 17 10
      app/i18n/en.php
  16. 16 9
      app/i18n/fr.php
  17. 3 4
      app/i18n/install.en.php
  18. 3 4
      app/i18n/install.fr.php
  19. 3 3
      app/layout/aside_feed.phtml
  20. 41 32
      app/layout/aside_flux.phtml
  21. 46 18
      app/layout/header.phtml
  22. 38 15
      app/layout/nav_menu.phtml
  23. 7 6
      app/views/configure/display.phtml
  24. 1 1
      app/views/configure/feed.phtml
  25. 25 19
      app/views/configure/users.phtml
  26. 1 1
      app/views/entry/bookmark.phtml
  27. 1 1
      app/views/entry/read.phtml
  28. 43 46
      app/views/helpers/javascript_vars.phtml
  29. 11 2
      app/views/helpers/view/global_view.phtml
  30. 72 55
      app/views/helpers/view/normal_view.phtml
  31. 3 2
      app/views/helpers/view/reader_view.phtml
  32. 1 1
      app/views/index/about.phtml
  33. 43 0
      app/views/index/formLogin.phtml
  34. 2 12
      app/views/index/index.phtml
  35. 8 0
      app/views/javascript/nbUnreadsPerFeed.phtml
  36. 2 0
      app/views/javascript/nonce.phtml
  37. 1 1
      constants.php
  38. 7 5
      lib/Minz/Configuration.php
  39. 1 1
      lib/Minz/FrontController.php
  40. 3 3
      lib/Minz/View.php
  41. 3 11
      lib/lib_rss.php
  42. 279 0
      lib/password_compat.php
  43. 1 14
      p/f.php
  44. 1 0
      p/i/index.php
  45. 82 7
      p/i/install.php
  46. 3 0
      p/index.html
  47. 41 0
      p/scripts/bcrypt.min.js
  48. 15 2
      p/scripts/global_view.js
  49. 169 97
      p/scripts/main.js
  50. 862 0
      p/themes/Dark/freshrss.css
  51. 491 0
      p/themes/Dark/global.css
  52. BIN
      p/themes/Dark/loader.gif
  53. 7 0
      p/themes/Dark/metadata.json
  54. 7 17
      p/themes/Flat/freshrss.css
  55. 14 4
      p/themes/Flat/global.css
  56. 0 0
      p/themes/Flat/icons/add.svg
  57. 0 0
      p/themes/Flat/icons/all.svg
  58. 0 0
      p/themes/Flat/icons/close.svg
  59. 0 0
      p/themes/Flat/icons/configure.svg
  60. 0 0
      p/themes/Flat/icons/down.svg
  61. 0 0
      p/themes/Flat/icons/next.svg
  62. 0 0
      p/themes/Flat/icons/prev.svg
  63. 0 0
      p/themes/Flat/icons/refresh.svg
  64. 0 0
      p/themes/Flat/icons/search.svg
  65. 0 0
      p/themes/Flat/icons/up.svg
  66. 0 0
      p/themes/Flat/loader.gif
  67. 0 0
      p/themes/Flat/metadata.json
  68. 7 21
      p/themes/Origine/freshrss.css
  69. 14 4
      p/themes/Origine/global.css
  70. 0 0
      p/themes/Origine/loader.gif
  71. 1 1
      p/themes/Origine/metadata.json

+ 19 - 6
CHANGELOG

@@ -5,12 +5,19 @@
 * Nouveau mode multi-utilisateur
 	* L’utilisateur par défaut (administrateur) peut créer et supprimer d’autres utilisateurs
 	* Nécessite un contrôle d’accès, soit :
+		* par le nouveau mode de connexion par formulaire (nom d’utilisateur + mot de passe)
+			* relativement sûr même sans HTTPS (le mot de passe n’est 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 d’utilisateur HTTP doit correspondre au nom d’utilisateur FreshRSS
 		* par Mozilla Persona, en renseignant l’adresse 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 “./data/config.php” “./data/*_user.php”
+	* 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 “./data/config.php” et “./data/*_user.php”,
+		éventuellement “./data/persona/*”
+* Rafraîchissement automatique du nombre d’articles non lus toutes les minutes (utilise le cache HTTP à bon escient)
+	* Permet aussi de conserver la session valide, surtout dans le cas de Persona
 * Importation OPML instantanée et plus tolérante
 * Nouvelle gestion des favicons avec téléchargement en parallèle
 * Nouvelles options
@@ -24,11 +31,13 @@
 	* Permet de modifier la description et l’adresse d’un 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 d’affichage : 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 d’utilisateur afin de permettre le mode multi-utilisateurs
 		* Amélioration des performances
@@ -38,16 +47,19 @@
 	* 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 (preload="none", et réécriture correcte des adresses, aussi en HTTPS)
+	* 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 d’articles sur des écrans étroits
 	* Meilleure accessibilité
 		* FreshRSS fonctionne aussi en mode dégradé sans images (alternatives Unicode) et/ou sans CSS
-	* Diverses améliorations mineures
+	* Diverses améliorations
 * PHP :
 	* Encore plus tolérant pour les flux comportant des erreurs
 	* Mise à jour automatique de l’URL du flux (en base de données) lorsque SimplePie découvre qu’elle a changé
@@ -56,15 +68,16 @@
 	* Amélioration des performances
 	* Chargement automatique des classes
 	* Alternative dans le cas d’absence de librairie JSON
-	* Pour le développement, le cache HTTP peut être désactivé en créant un fichier “./no-cache.txt”
+	* 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 !
+		* 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)

+ 19 - 2
README.md

@@ -8,7 +8,7 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an
 * Site officiel : http://freshrss.org
 * Démo : http://marienfressinaud.fr/projets/freshrss/
 * Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
-* Version actuelle : 0.7-beta4
+* Version actuelle : 0.7-RC1
 * Date de publication 2014-01-xx
 * License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
 
@@ -25,7 +25,7 @@ Privilégiez pour cela des demandes sur GitHub
 * 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+ (PHP 5.3.4+ recommandé)
+* PHP 5.2+ (PHP 5.3.7+ recommandé)
 	* Requis : [PDO_MySQL](http://php.net/pdo-mysql), [cURL](http://php.net/curl), [LibXML](http://php.net/xml), [PCRE](http://php.net/pcre), [ctype](http://php.net/ctype)
 	* Recommandés : [JSON](http://php.net/json), [zlib](http://php.net/zlib), [mbstring](http://php.net/mbstring), [iconv](http://php.net/iconv)
 * MySQL 5.0.3+ (ou SQLite 3.7.4+ à venir)
@@ -72,3 +72,20 @@ Par exemple, pour exécuter le script toutes les heures :
 ```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/)
+
+## 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)

+ 14 - 10
app/Controllers/configureController.php

@@ -66,7 +66,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		$this->view->feeds = $feedDAO->listFeeds ();
 		$this->view->flux = false;
 
-		Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' - ');
+		Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' · ');
 	}
 
 	public function feedAction () {
@@ -133,10 +133,10 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 					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 () . ' - ');
+				Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' — ' . $this->view->flux->name () . ' · ');
 			}
 		} else {
-			Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' - ');
+			Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' · ');
 		}
 	}
 
@@ -157,7 +157,11 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 				'scroll' => Minz_Request::param('mark_scroll', false),
 				'reception' => Minz_Request::param('mark_upon_reception', false),
 			));
-			$this->view->conf->_theme(Minz_Request::param('theme', 'default'));
+			$themeId = Minz_Request::param('theme', '');
+			if ($themeId == '') {
+				$themeId = FreshRSS_Themes::defaultTheme;
+			}
+			$this->view->conf->_theme($themeId);
 			$this->view->conf->_topline_read(Minz_Request::param('topline_read', false));
 			$this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false));
 			$this->view->conf->_topline_date(Minz_Request::param('topline_date', false));
@@ -185,7 +189,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 
 		$this->view->themes = FreshRSS_Themes::get();
 
-		Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' - ');
+		Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · ');
 	}
 
 	public function sharingAction () {
@@ -212,7 +216,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true);
 		}
 
-		Minz_View::prependTitle (Minz_Translate::t ('sharing_management') . ' - ');
+		Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · ');
 	}
 
 	public function importExportAction () {
@@ -277,7 +281,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		// au niveau de la vue, permet de ne pas voir un flux sélectionné dans la liste
 		$this->view->flux = false;
 
-		Minz_View::prependTitle (Minz_Translate::t ('import_export_opml') . ' - ');
+		Minz_View::prependTitle (Minz_Translate::t ('import_export_opml') . ' · ');
 	}
 
 	public function shortcutAction () {
@@ -313,11 +317,11 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			Minz_Request::forward (array ('c' => 'configure', 'a' => 'shortcut'), true);
 		}
 
-		Minz_View::prependTitle (Minz_Translate::t ('shortcuts_management') . ' - ');
+		Minz_View::prependTitle (Minz_Translate::t ('shortcuts') . ' · ');
 	}
 
 	public function usersAction() {
-		Minz_View::prependTitle(Minz_Translate::t ('users') . ' - ');
+		Minz_View::prependTitle(Minz_Translate::t ('users') . ' · ');
 	}
 
 	public function archivingAction () {
@@ -339,7 +343,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			Minz_Request::forward(array('c' => 'configure', 'a' => 'archiving'), true);
 		}
 
-		Minz_View::prependTitle(Minz_Translate::t('archiving_configuration') . ' - ');
+		Minz_View::prependTitle(Minz_Translate::t('archiving_configuration') . ' · ');
 
 		$entryDAO = new FreshRSS_EntryDAO();
 		$this->view->nb_total = $entryDAO->count();

+ 8 - 5
app/Controllers/entryController.php

@@ -10,6 +10,11 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		}
 
 		$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) {
@@ -34,13 +39,10 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$this->redirect = true;
 
 		$id = Minz_Request::param ('id');
-		$is_read = Minz_Request::param ('is_read');
 		$get = Minz_Request::param ('get');
 		$nextGet = Minz_Request::param ('nextGet', $get); 
 		$idMax = Minz_Request::param ('idMax', 0);
 
-		$is_read = (bool)$is_read;
-
 		$entryDAO = new FreshRSS_EntryDAO ();
 		if ($id == false) {
 			if (!$get) {
@@ -63,7 +65,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 						break;
 				}
 				if ($nextGet !== 'a') {
-					$this->params = array ('get' => $nextGet);
+					$this->params['get'] = $nextGet;
 				}
 			}
 
@@ -73,6 +75,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 			);
 			Minz_Session::_param ('notification', $notif);
 		} else {
+			$is_read = (bool)(Minz_Request::param ('is_read', true));
 			$entryDAO->markRead ($id, $is_read);
 		}
 	}
@@ -83,7 +86,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$id = Minz_Request::param ('id');
 		if ($id) {
 			$entryDAO = new FreshRSS_EntryDAO ();
-			$entryDAO->markFavorite ($id, Minz_Request::param ('is_favorite'));
+			$entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true)));
 		}
 	}
 

+ 1 - 1
app/Controllers/errorController.php

@@ -21,6 +21,6 @@ class FreshRSS_error_Controller extends Minz_ActionController {
 		
 		$this->view->logs = Minz_Request::param ('logs');
 		
-		Minz_View::prependTitle ($this->view->code . ' - ');
+		Minz_View::prependTitle ($this->view->code . ' · ');
 	}
 }

+ 3 - 2
app/Controllers/feedController.php

@@ -30,8 +30,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$cat = $def_cat->id ();
 			}
 
-			$user = Minz_Request::param ('username');
-			$pass = Minz_Request::param ('password');
+			$user = Minz_Request::param ('http_user');
+			$pass = Minz_Request::param ('http_pass');
 			$params = array ();
 
 			$transactionStarted = false;
@@ -164,6 +164,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$feedDAO = new FreshRSS_FeedDAO ();
 		$entryDAO = new FreshRSS_EntryDAO ();
 
+		Minz_Session::_param('actualize_feeds', false);
 		$id = Minz_Request::param ('id');
 		$force = Minz_Request::param ('force', false);
 

+ 55 - 11
app/Controllers/indexController.php

@@ -46,12 +46,8 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			// no layout for RSS output
 			$this->view->_useLayout (false);
 			header('Content-Type: application/rss+xml; charset=utf-8');
-		} else {
-			Minz_View::appendScript (Minz_Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
-
-			if ($output === 'global') {
-				Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
-			}
+		} elseif ($output === 'global') {
+			Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
 		}
 
 		$this->view->cat_aside = $this->catDAO->listCategories ();
@@ -83,7 +79,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		Minz_View::prependTitle (
 			$this->view->currentName .
 			($this->nb_not_read_cat > 0 ? ' (' . $this->nb_not_read_cat . ')' : '') .
-			' - '
+			' · '
 		);
 
 		// On récupère les différents éléments de filtrage
@@ -204,7 +200,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 	}
 
 	public function aboutAction () {
-		Minz_View::prependTitle (Minz_Translate::t ('about') . ' - ');
+		Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
 	}
 
 	public function logsAction () {
@@ -215,7 +211,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			);
 		}
 
-		Minz_View::prependTitle (Minz_Translate::t ('logs') . ' - ');
+		Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · ');
 
 		if (Minz_Request::isPost ()) {
 			FreshRSS_LogDAO::truncate();
@@ -290,8 +286,56 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 	}
 
 	public function logoutAction () {
-		$this->view->_useLayout (false);
-		Minz_Session::_param ('mail');
+		$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);
+				}
+			}
+			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);
+		}
+		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);
 	}
 }

+ 34 - 1
app/Controllers/javascriptController.php

@@ -3,11 +3,44 @@
 class FreshRSS_javascript_Controller extends Minz_ActionController {
 	public function firstAction () {
 		$this->view->_useLayout (false);
-		header('Content-type: text/javascript');
 	}
 
 	public function actualizeAction () {
+		header('Content-Type: text/javascript; charset=UTF-8');
 		$feedDAO = new FreshRSS_FeedDAO ();
 		$this->view->feeds = $feedDAO->listFeeds ();
 	}
+
+	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('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
+			}
+		}
+		$this->view->nonce = '';	//Failure
+		$this->view->salt1 = '';
+	}
 }

+ 41 - 5
app/Controllers/usersController.php

@@ -1,6 +1,9 @@
 <?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(
@@ -14,13 +17,29 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 		if (Minz_Request::isPost()) {
 			$ok = true;
 
-			$mail = Minz_Request::param('mail_login', false);
-			$this->view->conf->_mail_login($mail);
-			$ok &= $this->view->conf->save();
+			$passwordPlain = Minz_Request::param('passwordPlain', false);
+			if ($passwordPlain != '') {
+				Minz_Request::_param('passwordPlain');	//Discard plain-text password ASAP
+				$_POST['passwordPlain'] = '';
+				if (!function_exists('password_hash')) {
+					include_once(LIB_PATH . '/password_compat.php');
+				}
+				$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
+				$passwordPlain = '';
+				$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
+				$ok &= ($passwordHash != '');
+				$this->view->conf->_passwordHash($passwordHash);
+			}
+			Minz_Session::_param('passwordHash', $this->view->conf->passwordHash);
 
+			if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+				$this->view->conf->_mail_login(Minz_Request::param('mail_login', false));
+			}
 			$email = $this->view->conf->mail_login;
 			Minz_Session::_param('mail', $email);
 
+			$ok &= $this->view->conf->save();
+
 			if ($email != '') {
 				$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
 				@unlink($personaFile);
@@ -38,8 +57,8 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 				$auth_type = Minz_Request::param('auth_type', 'none');
 				if ($anon != Minz_Configuration::allowAnonymous() ||
 					$auth_type != Minz_Configuration::authType()) {
-					Minz_Configuration::_allowAnonymous($anon);
 					Minz_Configuration::_authType($auth_type);
+					Minz_Configuration::_allowAnonymous($anon);
 					$ok &= Minz_Configuration::writeFile();
 				}
 			}
@@ -76,10 +95,26 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 				$ok &= !file_exists($configPath);
 			}
 			if ($ok) {
+			
+				$passwordPlain = Minz_Request::param('new_user_passwordPlain', false);
+				$passwordHash = '';
+				if ($passwordPlain != '') {
+					Minz_Request::_param('new_user_passwordPlain');	//Discard plain-text password ASAP
+					$_POST['new_user_passwordPlain'] = '';
+					if (!function_exists('password_hash')) {
+						include_once(LIB_PATH . '/password_compat.php');
+					}
+					$passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
+					$passwordPlain = '';
+					$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 = '';
-					$ok &= Minz_Configuration::authType() !== 'persona';
 				} else {
 					$personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt';
 					@unlink($personaFile);
@@ -89,6 +124,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 			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);

+ 24 - 7
app/FreshRSS.php

@@ -4,15 +4,20 @@ class FreshRSS extends Minz_FrontController {
 		if (!isset($_SESSION)) {
 			Minz_Session::init('FreshRSS');
 		}
-		$this->accessControl(Minz_Session::param('currentUser', ''));
+		$loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
 		$this->loadParamsView();
-		$this->loadStylesAndScripts();	//TODO: Do not load that when not needed, e.g. some Ajax requests
+		$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 != '';
@@ -73,6 +78,9 @@ class FreshRSS extends Minz_FrontController {
 
 		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;
@@ -92,6 +100,7 @@ class FreshRSS extends Minz_FrontController {
 			}
 		}
 		Minz_View::_param ('loginOk', $loginOk);
+		return $loginOk;
 	}
 
 	private function loadParamsView () {
@@ -104,22 +113,30 @@ class FreshRSS extends Minz_FrontController {
 		}
 	}
 
-	private function loadStylesAndScripts () {
-		$theme = FreshRSS_Themes::get_infos($this->conf->theme);
+	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['path'] . '/' . $file . '?' . @filemtime(PUBLIC_PATH . '/themes/' . $theme['path'] . '/' . $file)));
+				Minz_View::appendStyle (Minz_Url::display ('/themes/' . $theme['id'] . '/' . $file . '?' . @filemtime(PUBLIC_PATH . '/themes/' . $theme['id'] . '/' . $file)));
 			}
 		}
 
-		if (Minz_Configuration::authType() === 'persona') {
-			Minz_View::appendScript ('https://login.persona.org/include.js');
+		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')));
 	}
 

+ 5 - 1
app/Models/Configuration.php

@@ -9,6 +9,7 @@ class FreshRSS_Configuration {
 		'keep_history_default' => 0,
 		'mail_login' => '',
 		'token' => '',
+		'passwordHash' => '',	//CRYPT_BLOWFISH
 		'posts_per_page' => 20,
 		'view_mode' => 'normal',
 		'default_view' => 'not_read',
@@ -24,7 +25,7 @@ class FreshRSS_Configuration {
 			'scroll' => false,
 			'reception' => false,
 		),
-		'theme' => 'default',
+		'theme' => 'Origine',
 		'shortcuts' => array(
 			'mark_read' => 'r',
 			'mark_favorite' => 'f',
@@ -162,6 +163,9 @@ class FreshRSS_Configuration {
 			}
 		}
 	}
+	public function _passwordHash ($value) {
+		$this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
+	}
 	public function _mail_login ($value) {
 		$value = filter_var($value, FILTER_VALIDATE_EMAIL);
 		if ($value) {

+ 1 - 1
app/Models/Entry.php

@@ -159,7 +159,7 @@ class FreshRSS_Entry extends Minz_Model {
 				try {
 					// l'article n'est pas en BDD, on va le chercher sur le site
 					$this->content = get_content_by_parsing(
-						$this->link(), $pathEntries
+						htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries
 					);
 				} catch (Exception $e) {
 					// rien à faire, on garde l'ancien contenu (requête a échoué)

+ 3 - 0
app/Models/EntryDAO.php

@@ -293,6 +293,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 			case 'read':
 				$where .= 'AND e1.is_read = 1 ';
 				break;
+			case 'favorite':
+				$where .= 'AND e1.is_favorite = 1 ';
+				break;
 			default:
 				throw new FreshRSS_EntriesGetter_Exception ('Bad state in Entry->listByType: [' . $state . ']!');
 		}

+ 23 - 5
app/Models/Themes.php

@@ -3,13 +3,17 @@
 class FreshRSS_Themes extends Minz_Model {
 	private static $themesUrl = '/themes/';
 	private static $defaultIconsUrl = '/themes/icons/';
+	public static $defaultTheme = 'Origine';
 
-	public static function get() {
-		$themes_list = array_diff(
+	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);
@@ -28,7 +32,7 @@ class FreshRSS_Themes extends Minz_Model {
 				$content = file_get_contents($json_filename);
 				$res = json_decode($content, true);
 				if ($res && isset($res['files']) && is_array($res['files'])) {
-					$res['path'] = $theme_id;
+					$res['id'] = $theme_id;
 					return $res;
 				}
 			}
@@ -39,12 +43,26 @@ class FreshRSS_Themes extends Minz_Model {
 	private static $themeIconsUrl;
 	private static $themeIcons;
 
-	public static function setThemeId($theme_id) {
+	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) {

+ 17 - 10
app/i18n/en.php

@@ -44,6 +44,8 @@ return array (
 	'rss_view'			=> 'RSS feed',
 	'show_all_articles'		=> 'Show all articles',
 	'show_not_reads'		=> 'Show only unread',
+	'show_read'			=> 'Show only read',
+	'show_favorite'			=> 'Show favorites',
 	'older_first'			=> 'Oldest first',
 	'newer_first'			=> 'Newer first',
 
@@ -155,21 +157,26 @@ return array (
 	'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'			=> 'Think to add RSS feeds!',
+	'think_to_add'			=> '<a href="./?c=configure&amp;a=feed">Remember to add some RSS feeds!</a>',
 
 	'current_user'			=> 'Current user',
-	'default_user'			=> 'Username of the default user (maximum 16 alphanumeric characters)',
-	'persona_connection_email'	=> 'Login mail address (for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)',
-	'allow_anonymous'		=> 'Allow anonymous reading for the default user (%s)',
+	'default_user'			=> 'Username of the default user <small>(maximum 16 alphanumeric characters)</small>',
+	'password_form'			=> 'Password<br /><small>(for the Web-form login method)</small>',
+	'persona_connection_email'	=> 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
+	'allow_anonymous'		=> 'Allow anonymous reading of the articles of the default user (%s)',
 	'auth_token'			=> 'Authentication token',
-	'explain_token'			=> 'Allows to access RSS output of the default user without authentication.<br /><kbd>%s?token=%s</kbd>',
+	'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',
@@ -236,7 +243,7 @@ return array (
 	'before_yesterday'		=> 'Before yesterday',
 	'by_author'			=> 'By <em>%s</em>',
 	'related_tags'			=> 'Related tags',
-	'no_feed_to_display'		=> 'No feed to show.',
+	'no_feed_to_display'		=> 'There is no feed to show yet.',
 
 	'about_freshrss'		=> 'About FreshRSS',
 	'project_website'		=> 'Project website',
@@ -255,8 +262,8 @@ return array (
 	'logs_empty'			=> 'Log file is empty',
 	'clear_logs'			=> 'Clear the logs',
 
-	'forbidden_access'		=> 'Forbidden access',
-	'forbidden_access_description'	=> 'Access is password protected, please <a class="signin" href="#">sign in</a> to read your feeds.',
+	'forbidden_access'		=> 'Access forbidden! (%s)',
+	'login_required'		=> 'Login required:',
 
 	'confirm_action'		=> 'Are you sure you want to perform this action? It cannot be cancelled!',
 
@@ -287,6 +294,6 @@ return array (
 	'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 dS Y',
-	'format_date_hour'		=> '%s dS Y \a\t H\.i',
+	'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',
 );

+ 16 - 9
app/i18n/fr.php

@@ -44,6 +44,8 @@ return array (
 	'rss_view'			=> 'Flux RSS',
 	'show_all_articles'		=> 'Afficher tous les articles',
 	'show_not_reads'		=> 'Afficher les non lus',
+	'show_read'			=> 'Afficher les lus',
+	'show_favorite'			=> 'Afficher les favoris',
 	'older_first'			=> 'Plus anciens en premier',
 	'newer_first'			=> 'Plus récents en premier',
 
@@ -155,21 +157,26 @@ return array (
 	'not_yet_implemented'		=> 'Pas encore implémenté',
 	'access_protected_feeds'	=> 'La connexion permet d’accéder aux flux protégés par une authentification HTTP',
 	'no_selected_feed'		=> 'Aucun flux sélectionné.',
-	'think_to_add'			=> 'Pensez à en ajouter !',
+	'think_to_add'			=> '<a href="./?c=configure&amp;a=feed">Pensez à en ajouter !</a>',
 
 	'current_user'			=> 'Utilisateur actuel',
-	'default_user'			=> 'Nom de l’utilisateur par défaut (16 caractères alphanumériques maximum)',
-	'persona_connection_email'	=> 'Adresse courriel de connexion (pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)',
-	'allow_anonymous'		=> 'Autoriser la lecture anonyme pour l’utilisateur par défaut (%s)',
+	'password_form'			=> 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+	'default_user'			=> 'Nom de l’utilisateur 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 l’utilisateur par défaut (%s)',
 	'auth_token'			=> 'Jeton d’identification',
 	'explain_token'			=> 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.<br /><kbd>%s?output=rss&token=%s</kbd>',
 	'login_configuration'		=> 'Identification',
 	'is_admin'			=> 'est administrateur',
 	'auth_type'			=> 'Méthode d’authentification',
 	'auth_none'			=> 'Aucune (dangereux)',
+	'auth_form'			=> 'Formulaire (traditionnel, requiert JavaScript)',
+	'http_auth'			=> 'HTTP (pour utilisateurs avancés avec HTTPS)',
+	'auth_persona'			=> 'Mozilla Persona (moderne, requiert JavaScript)',
 	'users_list'			=> 'Liste des utilisateurs',
 	'create_user'			=> 'Créer un nouvel utilisateur',
 	'username'			=> 'Nom d’utilisateur',
+	'password'			=> 'Mot de passe',
 	'create'			=> 'Créer',
 	'user_created'			=> 'L’utilisateur %s a été créé',
 	'user_deleted'			=> 'L’utilisateur %s a été supprimé',
@@ -236,7 +243,7 @@ return array (
 	'before_yesterday'		=> 'À partir d’avant-hier',
 	'by_author'			=> 'Par <em>%s</em>',
 	'related_tags'			=> 'Tags associés',
-	'no_feed_to_display'		=> 'Il n’y a aucun flux à afficher.',
+	'no_feed_to_display'		=> 'Il n’y a aucun flux à afficher pour l’instant.',
 
 	'about_freshrss'		=> 'À propos de FreshRSS',
 	'project_website'		=> 'Site du projet',
@@ -255,8 +262,8 @@ return array (
 	'logs_empty'			=> 'Les logs sont vides',
 	'clear_logs'			=> 'Effacer les logs',
 
-	'forbidden_access'		=> 'Accès interdit',
-	'forbidden_access_description'	=> 'L’accès est protégé par un mot de passe, veuillez <a class="signin" href="#">vous connecter</a> pour accéder aux flux.',
+	'forbidden_access'		=> 'Accès interdit ! (%s)',
+	'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 !',
 
@@ -287,6 +294,6 @@ return array (
 	'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'			=> 'd %s Y',
-	'format_date_hour'		=> '\l\e d %s Y \à H\:i',
+	'format_date'			=> 'j %s Y',
+	'format_date_hour'		=> '\l\e j %s Y \à H\:i',
 );

+ 3 - 4
app/i18n/install.en.php

@@ -1,8 +1,8 @@
 <?php
 return array (
-	'freshrss_installation'		=> 'Installation - FreshRSS',
+	'freshrss_installation'		=> 'Installation · FreshRSS',
 	'freshrss'			=> 'FreshRSS',
-	'installation_step'		=> 'Installation - step %d',
+	'installation_step'		=> 'Installation — step %d · FreshRSS',
 	'steps'				=> 'Steps',
 	'checks'			=> 'Checks',
 	'general_configuration'	=> 'General configuration',
@@ -40,6 +40,7 @@ return array (
 	'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.',
 
@@ -52,8 +53,6 @@ return array (
 	'bdd_conf_is_ok'		=> 'Database configuration has been saved.',
 	'bdd_conf_is_ko'		=> 'Verify your database information.',
 	'host'				=> 'Host',
-	'username'			=> 'Username',
-	'password'			=> 'Password',
 	'bdd'				=> 'Database',
 	'prefix'			=> 'Table prefix',
 

+ 3 - 4
app/i18n/install.fr.php

@@ -1,8 +1,8 @@
 <?php
 return array (
-	'freshrss_installation'		=> 'Installation - FreshRSS',
+	'freshrss_installation'		=> 'Installation · FreshRSS',
 	'freshrss'			=> 'FreshRSS',
-	'installation_step'		=> 'Installation - étape %d',
+	'installation_step'		=> 'Installation — étape %d · FreshRSS',
 	'steps'				=> 'Étapes',
 	'checks'			=> 'Vérifications',
 	'general_configuration'	=> 'Configuration générale',
@@ -40,6 +40,7 @@ return array (
 	'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.',
 
@@ -52,8 +53,6 @@ return array (
 	'bdd_conf_is_ok'		=> 'La configuration de la base de données a été enregistrée.',
 	'bdd_conf_is_ko'		=> 'Vérifiez les informations d’accès à la base de données.',
 	'host'				=> 'Hôte',
-	'username'			=> 'Nom utilisateur',
-	'password'			=> 'Mot de passe',
 	'bdd'				=> 'Base de données',
 	'prefix'			=> 'Préfixe des tables',
 

+ 3 - 3
app/layout/aside_feed.phtml

@@ -1,7 +1,7 @@
 <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')); ?>">
+	<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">
@@ -27,10 +27,10 @@
 
 					<li class="dropdown-header"><?php echo Minz_Translate::t ('http_authentication'); ?></li>
 					<li class="input">
-						<input type="text" name="username" id="username" placeholder="<?php echo Minz_Translate::t ('username'); ?>" />
+						<input type="text" name="http_user" id="http_user" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('username'); ?>" />
 					</li>
 					<li class="input">
-						<input type="password" name="password" id="password" placeholder="<?php echo Minz_Translate::t ('password'); ?>" />
+						<input type="password" name="http_pass" id="http_pass" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('password'); ?>" />
 					</li>
 				</ul>
 			</div>

+ 41 - 32
app/layout/aside_flux.phtml

@@ -13,9 +13,15 @@
 		<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">
-				<a data-unread="<?php echo $this->nb_not_read; ?>" class="btn<?php echo $this->get_c == 'a' ? ' active' : ''; ?>" href="<?php echo _url ('index', 'index'); ?>">
+				<a data-unread="<?php echo $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 ('all_feeds'); ?>
 				</a>
@@ -24,43 +30,46 @@
 
 		<li>
 			<div class="category favorites">
-				<a data-unread="<?php echo $this->nb_favorites['unread']; ?>" class="btn<?php echo $this->get_c == 's' ? ' active' : ''; ?>" href="<?php echo _url ('index', 'index', 'get', 's'); ?>">
+				<a data-unread="<?php echo $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', $this->nb_favorites['all']); ?>
 				</a>
 			</div>
 		</li>
 
-		<?php foreach ($this->cat_aside as $cat) { ?>
-		<?php $feeds = $cat->feeds (); ?>
-		<?php 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' : ''; ?>">
-				<a data-unread="<?php echo $cat->nbNotRead (); ?>" class="btn<?php echo $c_active ? ' active' : ''; ?>" href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id ()); ?>"><?php echo $cat->name (); ?></a>
-				<a class="btn dropdown-toggle" href="#"><?php echo FreshRSS_Themes::icon($c_active ? 'up' : 'down'); ?></a>
-			</div>
-
-			<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' : ''; ?>">
-					<div class="dropdown">
-						<div class="dropdown-target"></div>
-						<a class="dropdown-toggle" data-fweb="<?php echo $feed->website (); ?>"><?php echo FreshRSS_Themes::icon('configure'); ?></a>
-<?php /* feed_config_template */ ?>
-					</div>
-					<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" />
-					<a class="feed" data-unread="<?php echo $feed->nbNotRead (); ?>" data-priority="<?php echo $feed->priority (); ?>" href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed_id); ?>"><?php echo $feed->name(); ?></a>
-				</li>
-				<?php } ?>
-			</ul>
-		</li>
-		<?php } } ?>
+		<?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 $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 $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>
 
@@ -73,7 +82,7 @@
 		<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', 'is_read', 1, 'get', 'f_!!!!!!'); ?>"><?php echo Minz_Translate::t ('mark_read'); ?></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>

+ 46 - 18
app/layout/header.phtml

@@ -1,12 +1,25 @@
-<?php if (Minz_Configuration::canLogIn()) { ?>
-<ul class="nav nav-head nav-login">
-	<?php if ($this->loginOk) { ?>
-	<li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="#"><?php echo Minz_Translate::t ('logout'); ?></a></li>
-	<?php } else { ?>
-	<li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li>
-	<?php } ?>
-</ul>
-<?php } ?>
+<?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 FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="<?php echo _url ('index', 'formLogout'); ?>"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
+			} else {
+				?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="<?php echo _url ('index', 'formLogin'); ?>"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+			}
+			break;
+		case 'persona':
+			if ($this->loginOk) {
+				?><li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="#"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
+			} else {
+				?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+			}
+			break;
+	}
+	?></ul><?php
+}
+?>
 
 <div class="header">
 	<div class="item title">
@@ -62,16 +75,31 @@
 				<li class="separator"></li>
 				<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li>
 				<li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Minz_Translate::t ('logs'); ?></a></li>
-				<?php if (Minz_Configuration::canLogIn()) { ?>
-				<li class="separator"></li>
-				<li class="item"><a class="signout" href="#"><?php echo FreshRSS_Themes::icon('logout'); ?> <?php echo Minz_Translate::t ('logout'); ?></a></li>
-				<?php } ?>
+				<?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 FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::t ('logout'); ?></a></li><?php
+							break;
+						case 'persona':
+							?><li class="item"><a class="signout" href="#"><?php echo FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::t ('logout'); ?></a></li><?php
+							break;
+					}
+				} ?>
 			</ul>
 		</div>
 	</div>
-	<?php } elseif (Minz_Configuration::canLogIn()) { ?>
-	<div class="item configure">
-		<?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a>
-	</div>
-	<?php } ?>
+	<?php } elseif (Minz_Configuration::canLogIn()) {
+		?><div class="item configure"><?php
+		switch (Minz_Configuration::authType()) {
+			case 'form':
+				echo FreshRSS_Themes::icon('login'); ?><a class="signin" href="<?php echo _url ('index', 'formLogin'); ?>"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+				break;
+			case 'persona':
+				echo FreshRSS_Themes::icon('login'); ?><a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+				break;
+		}
+		?></div><?php
+	} ?>
 </div>

+ 38 - 15
app/layout/nav_menu.phtml

@@ -56,7 +56,13 @@
 		}
 		$p = isset($this->entries[0]) ? $this->entries[0] : null;
 		$idMax = $p === null ? '0' : $p->id();
-		$markReadUrl = _url ('entry', 'read', 'is_read', 1, 'get', $get, 'nextGet', $nextGet, 'idMax', $idMax);
+
+		$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);
 	?>
 
@@ -103,21 +109,21 @@
 				$url_output = $url;
 				$actual_view = Minz_Request::param('output', 'normal');
 			?>
-			<?php if($actual_view != 'normal') { ?>
+			<?php if($actual_view !== 'normal') { ?>
 			<li class="item">
 				<?php $url_output['params']['output'] = 'normal'; ?>
 				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
 					<?php echo Minz_Translate::t ('normal_view'); ?>
 				</a>
 			</li>
-			<?php } if($actual_view != 'reader') { ?>
+			<?php } if($actual_view !== 'reader') { ?>
 			<li class="item">
 				<?php $url_output['params']['output'] = 'reader'; ?>
 				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
 					<?php echo Minz_Translate::t ('reader_view'); ?>
 				</a>
 			</li>
-			<?php } if($actual_view != 'global') { ?>
+			<?php } if($actual_view !== 'global') { ?>
 			<li class="item">
 				<?php $url_output['params']['output'] = 'global'; ?>
 				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
@@ -128,24 +134,41 @@
 
 			<li class="separator"></li>
 
-			<li class="item">
-				<?php
-					$url_state = $url;
-					if ($this->state == 'not_read') {
-						$url_state['params']['state'] = 'all';
-				?>
+			<?php
+				$url_state = $url;
+				$url_state['params']['state'] = 'all';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'all') ? 'true' :'false'; ?>">
 				<a class="print_all" href="<?php echo Minz_Url::display ($url_state); ?>">
 					<?php echo Minz_Translate::t ('show_all_articles'); ?>
 				</a>
-				<?php
-					} else {
-						$url_state['params']['state'] = 'not_read';
-				?>
+			</li>
+			<?php
+				$url_state['params']['state'] = 'not_read';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'not_read') ? 'true' :'false'; ?>">
 				<a class="print_non_read" href="<?php echo Minz_Url::display ($url_state); ?>">
 					<?php echo Minz_Translate::t ('show_not_reads'); ?>
 				</a>
-				<?php } ?>
 			</li>
+			<?php
+				$url_state['params']['state'] = 'read';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'read') ? 'true' :'false'; ?>">
+				<a class="print_read" href="<?php echo Minz_Url::display ($url_state); ?>">
+					<?php echo Minz_Translate::t ('show_read'); ?>
+				</a>
+			</li>
+			<?php
+				$url_state['params']['state'] = 'favorite';
+			?>
+			<li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'favorite') ? 'true' :'false'; ?>">
+				<a class="print_favorite" href="<?php echo Minz_Url::display ($url_state); ?>">
+					<?php echo Minz_Translate::t ('show_favorite'); ?>
+				</a>
+			</li>
+
+			<li class="separator"></li>
 
 			<li class="item">
 				<?php

+ 7 - 6
app/views/configure/display.phtml

@@ -21,10 +21,11 @@
 		<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">
+				<select name="theme" id="theme" required="">
+				<option></option>
 				<?php foreach ($this->themes as $theme) { ?>
-				<option value="<?php echo $theme['path']; ?>"<?php echo $this->conf->theme === $theme['path'] ? ' selected="selected"' : ''; ?>>
-					<?php echo $theme['name'] . ' ' . Minz_Translate::t ('by') . ' ' . $theme['author']; ?> 
+				<option value="<?php echo $theme['id']; ?>"<?php echo $this->conf->theme === $theme['id'] ? ' selected="selected"' : ''; ?>>
+					<?php echo $theme['name'] . ' ' . Minz_Translate::t ('by') . ' ' . $theme['author']; ?> 
 				</option>
 				<?php } ?>
 				</select>
@@ -81,7 +82,7 @@
 				<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>
+					<noscript>  <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
 				</label>
 			</div>
 		</div>
@@ -91,7 +92,7 @@
 				<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>
+					<noscript>  <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
 				</label>
 			</div>
 		</div>
@@ -101,7 +102,7 @@
 				<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>
+					<noscript>  <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript>
 				</label>
 			</div>
 		</div>

+ 1 - 1
app/views/configure/feed.phtml

@@ -11,7 +11,7 @@
 	<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 } ?>
 
-	<form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>">
+	<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>

+ 25 - 19
app/views/configure/users.phtml

@@ -17,6 +17,14 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
+			<div class="group-controls">
+				<input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" />
+				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
 		<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; ?>
@@ -26,14 +34,12 @@
 			</div>
 		</div>
 
-		<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
 		<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 } ?>
 
 	<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
 
@@ -44,41 +50,35 @@
 			<div class="group-controls">
 				<select id="auth_type" name="auth_type" required="required">
 					<option value=""></option>
+					<option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : '', version_compare(PHP_VERSION, '5.3', '<') ? ' 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>
-					<option value="http_auth"<?php echo Minz_Configuration::authType() === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>>HTTP Auth</option>
-					<option value="persona"<?php echo Minz_Configuration::authType() === 'persona' ? ' selected="selected"' : '', $this->conf->mail_login == '' ? ' disabled="disabled"' : ''; ?>>Mozilla Persona</option>
 				</select>
-				<code>$_SERVER['REMOTE_USER'] = `<?php echo httpAuthUser(); ?>`</code>
-			</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::authType() === 'persona') { ?>
-
-		<legend>Mozilla Persona</legend>
 		<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"' : ''; ?> />
+					<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>
 
+		<?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'); ?>"/>
+				<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 form-actions">
 			<div class="group-controls">
@@ -86,8 +86,6 @@
 				<button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button>
 			</div>
 		</div>
-
-		<?php } ?>
 	</form>
 
 	<form method="post" action="<?php echo _url('users', 'delete'); ?>">
@@ -133,6 +131,14 @@
 			</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">
+				<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" pattern=".{7,}" />
+				<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; ?>

+ 1 - 1
app/views/entry/bookmark.phtml

@@ -1,7 +1,7 @@
 <?php
 header('Content-Type: application/json; charset=UTF-8');
 
-if (Minz_Request::param ('is_favorite')) {
+if (Minz_Request::param ('is_favorite', true)) {
 	Minz_Request::_param ('is_favorite', 0);
 } else {
 	Minz_Request::_param ('is_favorite', 1);

+ 1 - 1
app/views/entry/read.phtml

@@ -1,7 +1,7 @@
 <?php
 header('Content-Type: application/json; charset=UTF-8');
 
-if (Minz_Request::param ('is_read')) {
+if (Minz_Request::param ('is_read', true)) {
 	Minz_Request::_param ('is_read', 0);
 } else {
 	Minz_Request::_param ('is_read', 1);

+ 43 - 46
app/views/helpers/javascript_vars.phtml

@@ -1,47 +1,44 @@
 <?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',
-		',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';
-
-	$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'], '",',
-			'collapse_entry:"', $s['collapse_entry'], '",',
-			'load_more:"', $s['load_more'], '",',
-			'auto_share:"', $s['auto_share'], '"',
-		"},\n";
-
-	if (Minz_Request::param ('output') === 'global') {
-		echo "iconClose='", FreshRSS_Themes::icon('close'), "',\n";
-	}
-
-	$mail = Minz_Session::param ('mail', 'null');
-	if ($mail != 'null') {
-		$mail = '"' . $mail . '"';
-	}
-	echo 'use_persona=', Minz_Configuration::authType() === 'persona' ? 'true' : 'false',
-		',url_freshrss="', _url ('index', 'index'), '",',
-		'url_login="', _url ('index', 'login'), '",',
-		'url_logout="', _url ('index', 'logout'), '",',
-		'current_user_mail=', $mail, ",\n";
-
-	echo 'load_shortcuts=', Minz_Request::controllerName () === 'index' && Minz_Request::actionName () === 'index' ? 'true' : 'false', ",\n";
-
-	echo 'str_confirmation="', Minz_Translate::t('confirm_action'), '"', ",\n";
-
-	$autoActualise = Minz_Session::param('actualize_feeds', false);
-	echo 'auto_actualize_feeds=', $autoActualise ? 'true' : 'false', ";\n";
-	if ($autoActualise) {
-		Minz_Session::_param('actualize_feeds', false);
-	}
+
+echo '"use strict";', "\n";
+
+$mark = $this->conf->mark_when;
+echo 'var ',
+	'hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true',
+	',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';
+
+$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'], '",',
+	'collapse_entry:"', $s['collapse_entry'], '",',
+	'load_more:"', $s['load_more'], '",',
+	'auto_share:"', $s['auto_share'], '"',
+"},\n";
+
+if (Minz_Request::param ('output') === 'global') {
+	echo "iconClose='", FreshRSS_Themes::icon('close'), "',\n";
+}
+
+$authType = Minz_Configuration::authType();
+if ($authType === 'persona') {
+	echo 'current_user_mail="' . Minz_Session::param ('mail', '') . '",';
+}
+
+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";

+ 11 - 2
app/views/helpers/view/global_view.phtml

@@ -2,13 +2,22 @@
 
 <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 $cat->nbNotRead (); ?>" class="btn" href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id (), 'output', 'normal'); ?>">
+			<a data-unread="<?php echo $cat->nbNotRead (); ?>" class="btn" href="<?php $arUrl['params']['get'] = 'c_' . $cat->id (); echo Minz_Url::display($arUrl); ?>">
 			<?php echo $cat->name(); ?>
 			</a>
 		</div>
@@ -17,7 +26,7 @@
 			<?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 $feed->nbNotRead (); ?>" data-priority="<?php echo $feed->priority (); ?>" href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id (), 'output', 'normal'); ?>">
+				<a class="feed" data-unread="<?php echo $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>

+ 72 - 55
app/views/helpers/view/normal_view.phtml

@@ -37,50 +37,59 @@ if (!empty($this->entries)) {
 	$bottomline_link = $this->conf->bottomline_link;
 ?>
 
-<div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>">
-	<?php foreach ($this->entries as $item) { ?>
+<div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>"><?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;
+		}
 
-	<?php 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>
-		<span class="name"><?php echo $this->currentName; ?></span>
-	</div>
-	<?php $display_today = false; } ?>
-	<?php 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>
-		<span class="name"><?php echo $this->currentName; ?></span>
-	</div>
-	<?php $display_yesterday = false; } ?>
-	<?php 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>
-	</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 (); ?>">
+	?><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
-						?><a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', $item->isRead () ? 0 : 1); ?>"><?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
-						?><a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', $item->isFavorite () ? 0 : 1); ?>"><?php
+					?><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 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 } ?>
@@ -103,24 +112,30 @@ if (!empty($this->entries)) {
 				if ($this->loginOk) {
 					if ($bottomline_read) {
 						?><li class="item manage"><?php
-							?><a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', $item->isRead () ? 0 : 1); ?>"><?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
-							?><a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', $item->isFavorite () ? 0 : 1); ?>"><?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
+				<li class="item"><?php
 						if ($bottomline_sharing) {
 							$link = urlencode ($item->link ());
-							$title = urlencode ($item->title () . ' - ' . $feed->name ());
-					?>
-					<div class="dropdown">
+							$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'); ?>
@@ -181,29 +196,30 @@ if (!empty($this->entries)) {
 						</ul>
 					</div>
 					<?php } ?>
-				</li>
-				<?php
-					$tags = $bottomline_tags ? $item->tags() : null;
-					if (!empty($tags)) {
-				?>
-				<li class="item">
+				</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'); ?>
-							<?php echo Minz_Translate::t ('related_tags'); ?>
-						</a>
+						<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 } ?>
+							<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 } ?>
-				<?php if ($bottomline_date) { ?><li class="item date"><?php echo $item->date (); ?> </li><?php } ?>
-				<?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 } ?>
+				</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>
@@ -217,5 +233,6 @@ if (!empty($this->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 } ?>
+<?php } ?>

+ 3 - 2
app/views/helpers/view/reader_view.phtml

@@ -22,7 +22,7 @@ if (!empty($this->entries)) {
 
 				<div class="author">
 					<?php $author = $item->author (); ?>
-					<?php echo $author != '' ? Minz_Translate::t ('by_author', $author) . ' - ' : ''; ?>
+					<?php echo $author != '' ? Minz_Translate::t ('by_author', $author) . '  ' : ''; ?>
 					<?php echo $item->date (); ?>
 				</div>
 
@@ -44,5 +44,6 @@ if (!empty($this->entries)) {
 <?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 } ?>
+<?php } ?>

+ 1 - 1
app/views/index/about.phtml

@@ -8,7 +8,7 @@
 		<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>
+		<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>

+ 43 - 0
app/views/index/formLogin.phtml

@@ -0,0 +1,43 @@
+<div class="post content">
+
+<?php
+if (Minz_Configuration::canLogIn()) {
+	?><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 class="form-group">
+				<label class="group-name" for="username"><?php echo Minz_Translate::t('username'); ?></label>
+				<div class="group-controls">
+					<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
+				</div>
+			</div>
+			<div class="form-group">
+				<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
+				<div class="group-controls">
+					<input type="password" id="passwordPlain" required="required" />
+					<input type="hidden" id="challenge" name="challenge" />
+					<noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript>
+				</div>
+			</div>
+			<div class="form-group form-actions">
+				<div class="group-controls">
+					<button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button>
+				</div>
+			</div>
+		</form><?php
+			break;
+
+		case 'persona':
+			?><p><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t('login'); ?></a></p><?php
+			break;
+	}
+} else {
+	?><h1>FreshRSS</h1>
+	<p><?php echo Minz_Translate::t('forbidden_access', Minz_Configuration::authType()); ?></p><?php
+}
+?>
+
+	<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p>
+</div>

+ 2 - 12
app/views/index/index.phtml

@@ -1,15 +1,5 @@
 <?php
 
-function showForbidden() {
-?><div class="post content">
-	<h1><?php echo Minz_Translate::t ('forbidden_access'); ?></h1>
-	<p><?php echo Minz_Configuration::canLogIn() ?
-		Minz_Translate::t ('forbidden_access_description') :
-		Minz_Translate::t ('forbidden_access') . ' (' . Minz_Configuration::authType() . ')'; ?></p>
-	<p><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about_freshrss'); ?></a></p>
-</div><?php
-}
-
 $output = Minz_Request::param ('output', 'normal');
 
 if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
@@ -31,8 +21,8 @@ if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
 	if ($token_is_ok) {
 		$this->renderHelper ('view/rss_view');
 	} else {
-		showForbidden();
+		Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true);
 	}
 } else {
-	showForbidden();
+	Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true);
 }

+ 8 - 0
app/views/javascript/nbUnreadsPerFeed.phtml

@@ -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);

+ 2 - 0
app/views/javascript/nonce.phtml

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

+ 1 - 1
constants.php

@@ -1,5 +1,5 @@
 <?php
-define('FRESHRSS_VERSION', '0.7-beta4');
+define('FRESHRSS_VERSION', '0.7-RC1');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 
 // Constantes de chemins

+ 7 - 5
lib/Minz/Configuration.php

@@ -109,21 +109,23 @@ class Minz_Configuration {
 		return self::$auth_type !== 'none';
 	}
 	public static function canLogIn() {
-		return self::$auth_type === 'persona';
+		return self::$auth_type === 'form' || self::$auth_type === 'persona';
 	}
 
 	public static function _allowAnonymous($allow = false) {
-		self::$allow_anonymous = (bool)$allow;
+		self::$allow_anonymous = ((bool)$allow) && self::canLogIn();
 	}
 	public static function _authType($value) {
 		$value = strtolower($value);
 		switch ($value) {
+			case 'form':
 			case 'http_auth':
 			case 'persona':
 			case 'none':
 				self::$auth_type = $value;
 				break;
 		}
+		self::_allowAnonymous(self::$allow_anonymous);
 	}
 
 	/**
@@ -251,12 +253,12 @@ class Minz_Configuration {
 		if (isset ($general['default_user'])) {
 			self::$default_user = $general['default_user'];
 		}
-		if (isset ($general['allow_anonymous'])) {
-			self::$allow_anonymous = ((bool)($general['allow_anonymous'])) && ($general['allow_anonymous'] !== 'no');
-		}
 		if (isset ($general['auth_type'])) {
 			self::_authType($general['auth_type']);
 		}
+		if (isset ($general['allow_anonymous'])) {
+			self::$allow_anonymous = ((bool)($general['allow_anonymous'])) && ($general['allow_anonymous'] !== 'no');
+		}
 
 		// Base de données
 		if (isset ($ini_array['db'])) {

+ 1 - 1
lib/Minz/FrontController.php

@@ -34,7 +34,7 @@ class Minz_FrontController {
 	 */
 	public function __construct () {
 		if (LOG_PATH === false) {
-			$this->killApp ('Path doesn’t exist : LOG_PATH');
+			$this->killApp ('Path not found: LOG_PATH');
 		}
 
 		try {

+ 3 - 3
lib/Minz/View.php

@@ -63,7 +63,7 @@ class Minz_View {
 	 * Affiche la Vue en elle-même
 	 */
 	public function render () {
-		if ((@include($this->view_filename)) === false) {
+		if ((include($this->view_filename)) === false) {
 			Minz_Log::record ('File not found: `'
 			            . $this->view_filename . '`',
 			            Minz_Log::NOTICE);
@@ -79,7 +79,7 @@ class Minz_View {
 		             . self::LAYOUT_PATH_NAME . '/'
 		             . $part . '.phtml';
 
-		if ((@include($fic_partial)) === false) {
+		if ((include($fic_partial)) === false) {
 			Minz_Log::record ('File not found: `'
 			            . $fic_partial . '`',
 			            Minz_Log::WARNING);
@@ -95,7 +95,7 @@ class Minz_View {
 		            . '/views/helpers/'
 		            . $helper . '.phtml';
 
-		if ((@include($fic_helper)) === false) {;
+		if ((include($fic_helper)) === false) {;
 			Minz_Log::record ('File not found: `'
 			            . $fic_helper . '`',
 			            Minz_Log::WARNING);

+ 3 - 11
lib/lib_rss.php

@@ -110,6 +110,7 @@ function sanitizeHTML($data) {
 	static $simplePie = null;
 	if ($simplePie == null) {
 		$simplePie = new SimplePie();
+		$simplePie->init();
 	}
 	return html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_MAYBE_HTML));
 }
@@ -118,22 +119,13 @@ function sanitizeHTML($data) {
 function get_content_by_parsing ($url, $path) {
 	require_once (LIB_PATH . '/lib_phpQuery.php');
 
+	syslog(LOG_INFO, 'FreshRSS GET ' . $url);
 	$html = file_get_contents ($url);
 
 	if ($html) {
 		$doc = phpQuery::newDocument ($html);
 		$content = $doc->find ($path);
-		$content->find ('*')->removeAttr ('style')
-		                    ->removeAttr ('id')
-		                    ->removeAttr ('class')
-		                    ->removeAttr ('onload')
-		                    ->removeAttr ('target');
-		$content->removeAttr ('style')
-		        ->removeAttr ('id')
-		        ->removeAttr ('class')
-		        ->removeAttr ('onload')
-		        ->removeAttr ('target');
-		return $content->__toString ();
+		return sanitizeHTML($content->__toString());
 	} else {
 		throw new Exception ();
 	}

+ 279 - 0
lib/password_compat.php

@@ -0,0 +1,279 @@
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ */
+
+namespace {
+
+if (!defined('PASSWORD_DEFAULT')) {
+
+    define('PASSWORD_BCRYPT', 1);
+    define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+
+    /**
+     * Hash the password using the specified algorithm
+     *
+     * @param string $password The password to hash
+     * @param int    $algo     The algorithm to use (Defined by PASSWORD_* constants)
+     * @param array  $options  The options for the algorithm to use
+     *
+     * @return string|false The hashed password, or false on error.
+     */
+    function password_hash($password, $algo, array $options = array()) {
+        if (!function_exists('crypt')) {
+            trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+            return null;
+        }
+        if (!is_string($password)) {
+            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+            return null;
+        }
+        if (!is_int($algo)) {
+            trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+            return null;
+        }
+        $resultLength = 0;
+        switch ($algo) {
+            case PASSWORD_BCRYPT:
+                // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
+                $cost = 10;
+                if (isset($options['cost'])) {
+                    $cost = $options['cost'];
+                    if ($cost < 4 || $cost > 31) {
+                        trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+                        return null;
+                    }
+                }
+                // The length of salt to generate
+                $raw_salt_len = 16;
+                // The length required in the final serialization
+                $required_salt_len = 22;
+                $hash_format = sprintf("$2y$%02d$", $cost);
+                // The expected length of the final crypt() output
+                $resultLength = 60;
+                break;
+            default:
+                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+                return null;
+        }
+        $salt_requires_encoding = false;
+        if (isset($options['salt'])) {
+            switch (gettype($options['salt'])) {
+                case 'NULL':
+                case 'boolean':
+                case 'integer':
+                case 'double':
+                case 'string':
+                    $salt = (string) $options['salt'];
+                    break;
+                case 'object':
+                    if (method_exists($options['salt'], '__tostring')) {
+                        $salt = (string) $options['salt'];
+                        break;
+                    }
+                case 'array':
+                case 'resource':
+                default:
+                    trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
+                    return null;
+            }
+            if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
+                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
+                return null;
+            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+                $salt_requires_encoding = true;
+            }
+        } else {
+            $buffer = '';
+            $buffer_valid = false;
+            if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
+                $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
+                if ($buffer) {
+                    $buffer_valid = true;
+                }
+            }
+            if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+                $buffer = openssl_random_pseudo_bytes($raw_salt_len);
+                if ($buffer) {
+                    $buffer_valid = true;
+                }
+            }
+            if (!$buffer_valid && @is_readable('/dev/urandom')) {
+                $f = fopen('/dev/urandom', 'r');
+                $read = PasswordCompat\binary\_strlen($buffer);
+                while ($read < $raw_salt_len) {
+                    $buffer .= fread($f, $raw_salt_len - $read);
+                    $read = PasswordCompat\binary\_strlen($buffer);
+                }
+                fclose($f);
+                if ($read >= $raw_salt_len) {
+                    $buffer_valid = true;
+                }
+            }
+            if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
+                $bl = PasswordCompat\binary\_strlen($buffer);
+                for ($i = 0; $i < $raw_salt_len; $i++) {
+                    if ($i < $bl) {
+                        $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+                    } else {
+                        $buffer .= chr(mt_rand(0, 255));
+                    }
+                }
+            }
+            $salt = $buffer;
+            $salt_requires_encoding = true;
+        }
+        if ($salt_requires_encoding) {
+            // encode string with the Base64 variant used by crypt
+            $base64_digits =
+                'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+            $bcrypt64_digits =
+                './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+            $base64_string = base64_encode($salt);
+            $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
+        }
+        $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
+
+        $hash = $hash_format . $salt;
+
+        $ret = crypt($password, $hash);
+
+        if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
+            return false;
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Get information about the password hash. Returns an array of the information
+     * that was used to generate the password hash.
+     *
+     * array(
+     *    'algo' => 1,
+     *    'algoName' => 'bcrypt',
+     *    'options' => array(
+     *        'cost' => 10,
+     *    ),
+     * )
+     *
+     * @param string $hash The password hash to extract info from
+     *
+     * @return array The array of information about the hash.
+     */
+    function password_get_info($hash) {
+        $return = array(
+            'algo' => 0,
+            'algoName' => 'unknown',
+            'options' => array(),
+        );
+        if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
+            $return['algo'] = PASSWORD_BCRYPT;
+            $return['algoName'] = 'bcrypt';
+            list($cost) = sscanf($hash, "$2y$%d$");
+            $return['options']['cost'] = $cost;
+        }
+        return $return;
+    }
+
+    /**
+     * Determine if the password hash needs to be rehashed according to the options provided
+     *
+     * If the answer is true, after validating the password using password_verify, rehash it.
+     *
+     * @param string $hash    The hash to test
+     * @param int    $algo    The algorithm used for new password hashes
+     * @param array  $options The options array passed to password_hash
+     *
+     * @return boolean True if the password needs to be rehashed.
+     */
+    function password_needs_rehash($hash, $algo, array $options = array()) {
+        $info = password_get_info($hash);
+        if ($info['algo'] != $algo) {
+            return true;
+        }
+        switch ($algo) {
+            case PASSWORD_BCRYPT:
+                $cost = isset($options['cost']) ? $options['cost'] : 10;
+                if ($cost != $info['options']['cost']) {
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * Verify a password against a hash using a timing attack resistant approach
+     *
+     * @param string $password The password to verify
+     * @param string $hash     The hash to verify against
+     *
+     * @return boolean If the password matches the hash
+     */
+    function password_verify($password, $hash) {
+        if (!function_exists('crypt')) {
+            trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
+            return false;
+        }
+        $ret = crypt($password, $hash);
+        if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
+            return false;
+        }
+
+        $status = 0;
+        for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
+            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+        }
+
+        return $status === 0;
+    }
+}
+
+}
+
+namespace PasswordCompat\binary {
+    /**
+     * Count the number of bytes in a string
+     *
+     * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
+     * In this case, strlen() will count the number of *characters* based on the internal encoding. A
+     * sequence of bytes might be regarded as a single multibyte character.
+     *
+     * @param string $binary_string The input string
+     *
+     * @internal
+     * @return int The number of bytes
+     */
+    function _strlen($binary_string) {
+           if (function_exists('mb_strlen')) {
+               return mb_strlen($binary_string, '8bit');
+           }
+           return strlen($binary_string);
+    }
+
+    /**
+     * Get a substring based on byte limits
+     *
+     * @see _strlen()
+     *
+     * @param string $binary_string The input string
+     * @param int    $start
+     * @param int    $length
+     *
+     * @internal
+     * @return string The substring
+     */
+    function _substr($binary_string, $start, $length) {
+       if (function_exists('mb_substr')) {
+           return mb_substr($binary_string, $start, $length, '8bit');
+       }
+       return substr($binary_string, $start, $length);
+   }
+
+}

+ 1 - 14
p/f.php

@@ -7,13 +7,6 @@ function download_favicon ($website, $dest) {
 	$ok = false;
 	$url = 'http://g.etfv.co/' . $website;
 
-	/*if (!is_dir ($favicons_dir)) {
-		if (!mkdir ($favicons_dir, 0755, true)) {
-			header('Location: ' . $url);
-			return false;
-		}
-	}*/
-
 	$c = curl_init ($url);
 	curl_setopt ($c, CURLOPT_HEADER, false);
 	curl_setopt ($c, CURLOPT_RETURNTRANSFER, true);
@@ -36,13 +29,7 @@ function download_favicon ($website, $dest) {
 	return true;
 }
 
-if (isset($_SERVER['PATH_INFO'])) {
-	$id = substr($_SERVER['PATH_INFO'], 1);
-} elseif (isset($_SERVER['QUERY_STRING'])) {
-	$id = $_SERVER['QUERY_STRING'];
-} else {
-	$id = '0';
-}
+$id = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '0';
 
 if (!ctype_xdigit($id)) {
 	$id = '0';

+ 1 - 0
p/i/index.php

@@ -26,6 +26,7 @@ if (file_exists ('install.php')) {
 
 	session_cache_limiter('');
 	Minz_Session::init('FreshRSS');
+	Minz_Session::_param('keepAlive', 1);	//For Persona
 
 	if (!file_exists(DATA_PATH . '/no-cache.txt')) {
 		require(LIB_PATH . '/http-conditional.php');

+ 82 - 7
p/i/install.php

@@ -1,5 +1,11 @@
 <?php
+if (function_exists('opcache_reset')) {
+	opcache_reset();
+}
+
 require('../../constants.php');
+const BCRYPT_COST = 9;
+
 include(LIB_PATH . '/lib_rss.php');
 
 session_name('FreshRSS');
@@ -149,6 +155,7 @@ function saveStep2 () {
 	if (!empty ($_POST)) {
 		if (empty ($_POST['title']) ||
 		    empty ($_POST['old_entries']) ||
+		    empty ($_POST['auth_type']) ||
 		    empty ($_POST['default_user'])) {
 			return false;
 		}
@@ -161,6 +168,12 @@ function saveStep2 () {
 		}
 		$_SESSION['mail_login'] = filter_var($_POST['mail_login'], FILTER_VALIDATE_EMAIL);
 		$_SESSION['default_user'] = substr(preg_replace('/[^a-zA-Z0-9]/', '', $_POST['default_user']), 0, 16);
+		$_SESSION['auth_type'] = $_POST['auth_type'];
+		if (!empty($_POST['passwordPlain'])) {
+			$passwordHash = password_hash($_POST['passwordPlain'], PASSWORD_BCRYPT, array('cost' => BCRYPT_COST));
+			$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);	//Compatibility with bcrypt.js
+			$_SESSION['passwordHash'] = $passwordHash;
+		}
 
 		$token = '';
 		if ($_SESSION['mail_login']) {
@@ -169,8 +182,10 @@ function saveStep2 () {
 
 		$config_array = array (
 			'language' => $_SESSION['language'],
+			'theme' => $_SESSION['theme'],
 			'old_entries' => $_SESSION['old_entries'],
 			'mail_login' => $_SESSION['mail_login'],
+			'passwordHash' => $_SESSION['passwordHash'],
 			'token' => $token,
 		);
 
@@ -213,6 +228,8 @@ function saveStep3 () {
 				'base_url' => '',
 				'title' => $_SESSION['title'],
 				'default_user' => $_SESSION['default_user'],
+				'auth_type' => $_SESSION['auth_type'],
+				'allow_anonymous' => isset($_SESSION['allow_anonymous']) ? $_SESSION['allow_anonymous'] : false,
 			),
 			'db' => array(
 				'type' => $_SESSION['bd_type'],
@@ -423,8 +440,9 @@ function checkStep0 () {
 
 	if (file_exists(DATA_PATH . '/config.php')) {
 		$ini_array = include(DATA_PATH . '/config.php');
-	} elseif (file_exists(DATA_PATH . '/application.ini')) {
+	} elseif (file_exists(DATA_PATH . '/application.ini')) {	//v0.6
 		$ini_array = parse_ini_file(DATA_PATH . '/application.ini', true);
+		$ini_array['general']['title'] = empty($ini_array['general']['title']) ? '' : stripslashes($ini_array['general']['title']);
 	} else {
 		$ini_array = null;
 	}
@@ -432,7 +450,7 @@ function checkStep0 () {
 	if ($ini_array) {
 		$ini_general = isset($ini_array['general']) ? $ini_array['general'] : null;
 		if ($ini_general) {
-			$keys = array('environment', 'salt', 'title', 'default_user');
+			$keys = array('environment', 'salt', 'title', 'default_user', 'allow_anonymous', 'auth_type');
 			foreach ($keys as $key) {
 				if ((empty($_SESSION[$key])) && isset($ini_general[$key])) {
 					$_SESSION[$key] = $ini_general[$key];
@@ -454,11 +472,20 @@ function checkStep0 () {
 		$userConfig = include(DATA_PATH . '/' . $_SESSION['default_user'] . '_user.php');
 	} elseif (file_exists(DATA_PATH . '/Configuration.array.php')) {
 		$userConfig = include(DATA_PATH . '/Configuration.array.php');	//v0.6
+		if (empty($_SESSION['auth_type'])) {
+			$_SESSION['auth_type'] = empty($userConfig['mail_login']) ? 'none' : 'persona';
+		}
+		if (!isset($_SESSION['allow_anonymous'])) {
+			$_SESSION['allow_anonymous'] = empty($userConfig['anon_access']) ? false : ($userConfig['anon_access'] === 'yes');
+		}
 	} else {
 		$userConfig = array();
 	}
+	if (empty($_SESSION['auth_type'])) {	//v0.7b
+		$_SESSION['auth_type'] = '';
+	}
 
-	$keys = array('language', 'old_entries', 'mail_login');
+	$keys = array('language', 'theme', 'old_entries', 'mail_login', 'passwordHash');
 	foreach ($keys as $key) {
 		if ((!isset($_SESSION[$key])) && isset($userConfig[$key])) {
 			$_SESSION[$key] = $userConfig[$key];
@@ -469,6 +496,25 @@ function checkStep0 () {
 	$language = isset ($_SESSION['language']) &&
 	            isset ($languages[$_SESSION['language']]);
 
+	if (empty($_SESSION['passwordHash'])) {	//v0.7b
+		$_SESSION['passwordHash'] = '';
+	}
+	if (empty($_SESSION['theme'])) {
+		$_SESSION['theme'] = 'Origine';
+	} else {
+		switch (strtolower($_SESSION['theme'])) {
+			case 'default':	//v0.7b
+				$_SESSION['theme'] = 'Origine';
+				break;
+			case 'flat-design':	//v0.7b
+				$_SESSION['theme'] = 'Flat';
+				break;
+			case 'default_dark':	//v0.7b
+				$_SESSION['theme'] = 'Dark';
+				break;
+		}
+	}
+
 	return array (
 		'language' => $language ? 'ok' : 'ko',
 		'all' => $language ? 'ok' : 'ko'
@@ -486,6 +532,7 @@ function checkStep1 () {
 	$cache = CACHE_PATH && is_writable (CACHE_PATH);
 	$log = LOG_PATH && is_writable (LOG_PATH);
 	$favicons = is_writable (DATA_PATH . '/favicons');
+	$persona = is_writable (DATA_PATH . '/persona');
 
 	return array (
 		'php' => $php ? 'ok' : 'ko',
@@ -499,7 +546,8 @@ function checkStep1 () {
 		'cache' => $cache ? 'ok' : 'ko',
 		'log' => $log ? 'ok' : 'ko',
 		'favicons' => $favicons ? 'ok' : 'ko',
-		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $data && $cache && $log && $favicons ? 'ok' : 'ko'
+		'persona' => $persona ? 'ok' : 'ko',
+		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $data && $cache && $log && $favicons && $persona ? 'ok' : 'ko'
 	);
 }
 
@@ -709,6 +757,12 @@ function printStep1 () {
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('file_is_nok', DATA_PATH . '/favicons'); ?></p>
 	<?php } ?>
 
+	<?php if ($res['persona'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t ('ok'); ?></span> <?php echo _t ('persona_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('file_is_nok', DATA_PATH . '/persona'); ?></p>
+	<?php } ?>
+
 	<?php if ($res['all'] == 'ok') { ?>
 	<a class="btn btn-important next-step" href="?step=2"><?php echo _t ('next_step'); ?></a>
 	<?php } else { ?>
@@ -747,10 +801,31 @@ function printStep2 () {
 			</div>
 		</div>
 
+		<div class="form-group">
+			<label class="group-name" for="auth_type"><?php echo _t('auth_type'); ?></label>
+			<div class="group-controls">
+				<select id="auth_type" name="auth_type" required="required">
+					<option value=""></option>
+					<option value="form"<?php echo $_SESSION['auth_type'] === 'form' ? ' selected="selected"' : '', version_compare(PHP_VERSION, '5.3', '<') ? ' disabled="disabled"' : ''; ?>><?php echo _t('auth_form'); ?></option>
+					<option value="persona"<?php echo $_SESSION['auth_type'] === 'persona' ? ' selected="selected"' : ''; ?>><?php echo _t('auth_persona'); ?></option>
+					<option value="http_auth"<?php echo $_SESSION['auth_type'] === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>><?php echo _t('http_auth'); ?> (REMOTE_USER = '<?php echo httpAuthUser(); ?>')</option>
+					<option value="none"<?php echo $_SESSION['auth_type'] === 'none' ? ' selected="selected"' : ''; ?>><?php echo _t('auth_none'); ?></option>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="passwordPlain"><?php echo _t('password_form'); ?></label>
+			<div class="group-controls">
+				<input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" autocomplete="off" />
+				<noscript><b><?php echo _t('javascript_should_be_activated'); ?></b></noscript>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<label class="group-name" for="mail_login"><?php echo _t ('persona_connection_email'); ?></label>
 			<div class="group-controls">
-				<input type="email" id="mail_login" name="mail_login" value="<?php echo isset ($_SESSION['mail_login']) ? $_SESSION['mail_login'] : ''; ?>" placeholder="<?php echo _t ('blank_to_disable'); ?>" />
+				<input type="email" id="mail_login" name="mail_login" value="<?php echo isset ($_SESSION['mail_login']) ? $_SESSION['mail_login'] : ''; ?>" placeholder="alice@example.net" />
 				<noscript><b><?php echo _t ('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>
@@ -910,8 +985,8 @@ case 6:
 		<meta charset="utf-8">
 		<meta name="viewport" content="initial-scale=1.0">
 		<title><?php echo _t ('freshrss_installation'); ?></title>
-		<link rel="stylesheet" type="text/css" media="all" href="../themes/default/global.css" />
-		<link rel="stylesheet" type="text/css" media="all" href="../themes/default/freshrss.css" />
+		<link rel="stylesheet" type="text/css" media="all" href="../themes/Origine/global.css" />
+		<link rel="stylesheet" type="text/css" media="all" href="../themes/Origine/freshrss.css" />
 	</head>
 	<body>
 

+ 3 - 0
p/index.html

@@ -22,6 +22,9 @@ h1 a {
 	color: #0062BE;
 	text-decoration: none;
 }
+img {
+	border: 0;
+}
 </style>
 </head>
 

+ 41 - 0
p/scripts/bcrypt.min.js

@@ -0,0 +1,41 @@
+/*
+ bcrypt.js (c) 2013 Daniel Wirtz <dcode@dcode.io>
+ Released under the Apache License, Version 2.0
+ see: https://github.com/dcodeIO/bcrypt.js for details
+*/
+function p(n){throw n;}var q=null;
+(function(n){function u(c,a,b,f){for(var d,e=c[a],l=c[a+1],e=e^b[0],h=0;14>=h;)d=f[e>>24&255],d+=f[256|e>>16&255],d^=f[512|e>>8&255],d+=f[768|e&255],l^=d^b[++h],d=f[l>>24&255],d+=f[256|l>>16&255],d^=f[512|l>>8&255],d+=f[768|l&255],e^=d^b[++h];c[a]=l^b[17];c[a+1]=e;return c}function s(c,a){var b,f=0;for(b=0;4>b;b++)f=f<<8|c[a]&255,a=(a+1)%c.length;return{key:f,a:a}}function y(c,a,b){for(var f=0,d=[0,0],e=a.length,l=b.length,h=0;h<e;h++){var g=s(c,f),f=g.a;a[h]^=g.key}for(h=0;h<e;h+=2)d=u(d,0,a,b),
+a[h]=d[0],a[h+1]=d[1];for(h=0;h<l;h+=2)d=u(d,0,a,b),b[h]=d[0],b[h+1]=d[1]}function C(c,a,b,f){for(var d=0,e=[0,0],l=b.length,h=f.length,g,k=0;k<l;k++)g=s(a,d),d=g.a,b[k]^=g.key;for(k=d=0;k<l;k+=2)g=s(c,d),d=g.a,e[0]^=g.key,g=s(c,d),d=g.a,e[1]^=g.key,e=u(e,0,b,f),b[k]=e[0],b[k+1]=e[1];for(k=0;k<h;k+=2)g=s(c,d),d=g.a,e[0]^=g.key,g=s(c,d),d=g.a,e[1]^=g.key,e=u(e,0,b,f),f[k]=e[0],f[k+1]=e[1]}function z(c){"undefined"!==typeof process&&"function"===typeof process.nextTick?process.nextTick(c):setTimeout(c,
+0)}function A(c,a,b,f){function d(){if(k<b)for(var n=new Date;k<b&&!(k+=1,y(c,h,g),y(a,h,g),100<Date.now()-n););else{for(k=0;64>k;k++)for(m=0;m<l>>1;m++)u(e,m<<1,h,g);n=[];for(k=0;k<l;k++)n.push((e[k]>>24&255)>>>0),n.push((e[k]>>16&255)>>>0),n.push((e[k]>>8&255)>>>0),n.push((e[k]&255)>>>0);return f?(f(q,n),q):n}f&&z(d);return q}var e=B.slice(),l=e.length;(4>b||31<b)&&p(Error("Illegal number of rounds: "+b));16!=a.length&&p(Error("Illegal salt length: "+a.length+" != 16"));b=1<<b;var h=D.slice(),g=
+E.slice();C(a,c,h,g);var k=0,m;if("undefined"!==typeof f)return d(),q;for(var n;;)if((n=d())!==q)return n}function F(c){for(var a,b,f=[],d=0;d<c.length;d++){a=c.charCodeAt(d);b=[];do b.push(a&255),a>>=8;while(a);f=f.concat(b.reverse())}return f}function w(c,a,b){function f(a){var b=[];b.push("$2");"a"<=d&&b.push(d);b.push("$");10>l&&b.push("0");b.push(l.toString());b.push("$");b.push(v.b(h,h.length));b.push(v.b(a,4*B.length-1));return b.join("")}var d,e;("$"!=a.charAt(0)||"2"!=a.charAt(1))&&p(Error("Invalid salt version: "+
+a.substring(0,2)));"$"==a.charAt(2)?(d=String.fromCharCode(0),e=3):(d=a.charAt(2),("a"!=d||"$"!=a.charAt(3))&&p(Error("Invalid salt revision: "+a.substring(2,4))),e=4);"$"<a.charAt(e+2)&&p(Error("Missing salt rounds"));var l=10*parseInt(a.substring(e,e+1),10)+parseInt(a.substring(e+1,e+2),10);a=a.substring(e+3,e+25);c=F(c+("a"<=d?"\x00":""));var h=[],h=v.c(a,16);if("undefined"==typeof b)return f(A(c,h,l));A(c,h,l,function(a,c){a?b(a,q):b(q,f(c))});return q}function G(){if("undefined"!==typeof module&&
+module.exports)return require("crypto").randomBytes(16);var c=new Uint32Array(16);n.crypto&&"function"===typeof n.crypto.getRandomValues?n.crypto.getRandomValues(c):"function"===typeof x?x(c):p(Error("Failed to generate random values: Web Crypto API not available / no polyfill set"));return Array.prototype.slice.call(c)}var t="./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split(""),r=[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,54,55,56,57,58,59,60,61,62,63,-1,-1,-1,-1,-1,-1,-1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,-1,-1,-1,-1,-1,-1,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,-1,-1,-1,-1,-1],v={b:function(c,a){var b=0,f=[],d,e;for((0>=a||a>c.length)&&p(Error("Invalid 'len': "+a));b<a;){d=c[b++]&255;f.push(t[d>>2&63]);d=(d&3)<<4;if(b>=a){f.push(t[d&63]);break}e=c[b++]&255;d|=e>>4&15;f.push(t[d&63]);d=(e&15)<<
+2;if(b>=a){f.push(t[d&63]);break}e=c[b++]&255;d|=e>>6&3;f.push(t[d&63]);f.push(t[e&63])}return f.join("")},c:function(c,a){var b=0,f=c.length,d=0,e=[],l,h,g;for(0>=a&&p(Error("Illegal 'len': "+a));b<f-1&&d<a;){g=c.charCodeAt(b++);l=g<r.length?r[g]:-1;g=c.charCodeAt(b++);h=g<r.length?r[g]:-1;if(-1==l||-1==h)break;g=l<<2>>>0;g|=(h&48)>>4;e.push(String.fromCharCode(g));if(++d>=a||b>=f)break;g=c.charCodeAt(b++);l=g<r.length?r[g]:-1;if(-1==l)break;g=(h&15)<<4>>>0;g|=(l&60)>>2;e.push(String.fromCharCode(g));
+if(++d>=a||b>=f)break;g=c.charCodeAt(b++);h=g<r.length?r[g]:-1;g=(l&3)<<6>>>0;g|=h;e.push(String.fromCharCode(g));++d}f=[];for(b=0;b<d;b++)f.push(e[b].charCodeAt(0));return f}},m={},D=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731],E=[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,
+1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,
+1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,
+3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,
+3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005,3532540348,1274779536,3789419226,2764799539,1660621633,3471099624,4011903706,913787905,3497959166,737222580,2514213453,2928710040,3937242737,1804850592,3499020752,2949064160,2386320175,2390070455,2415321851,4061277028,2290661394,2416832540,1336762016,1754252060,3520065937,3014181293,791618072,3188594551,3933548030,2332172193,
+3852520463,3043980520,413987798,3465142937,3030929376,4245938359,2093235073,3534596313,375366246,2157278981,2479649556,555357303,3870105701,2008414854,3344188149,4221384143,3956125452,2067696032,3594591187,2921233993,2428461,544322398,577241275,1471733935,610547355,4027169054,1432588573,1507829418,2025931657,3646575487,545086370,48609733,2200306550,1653985193,298326376,1316178497,3007786442,2064951626,458293330,2589141269,3591329599,3164325604,727753846,2179363840,146436021,1461446943,4069977195,
+705550613,3059967265,3887724982,4281599278,3313849956,1404054877,2845806497,146425753,1854211946,1266315497,3048417604,3681880366,3289982499,290971E4,1235738493,2632868024,2414719590,3970600049,1771706367,1449415276,3266420449,422970021,1963543593,2690192192,3826793022,1062508698,1531092325,1804592342,2583117782,2714934279,4024971509,1294809318,4028980673,1289560198,2221992742,1669523910,35572830,157838143,1052438473,1016535060,1802137761,1753167236,1386275462,3080475397,2857371447,1040679964,2145300060,
+2390574316,1461121720,2956646967,4031777805,4028374788,33600511,2920084762,1018524850,629373528,3691585981,3515945977,2091462646,2486323059,586499841,988145025,935516892,3367335476,2599673255,2839830854,265290510,3972581182,2759138881,3795373465,1005194799,847297441,406762289,1314163512,1332590856,1866599683,4127851711,750260880,613907577,1450815602,3165620655,3734664991,3650291728,3012275730,3704569646,1427272223,778793252,1343938022,2676280711,2052605720,1946737175,3164576444,3914038668,3967478842,
+3682934266,1661551462,3294938066,4011595847,840292616,3712170807,616741398,312560963,711312465,1351876610,322626781,1910503582,271666773,2175563734,1594956187,70604529,3617834859,1007753275,1495573769,4069517037,2549218298,2663038764,504708206,2263041392,3941167025,2249088522,1514023603,1998579484,1312622330,694541497,2582060303,2151582166,1382467621,776784248,2618340202,3323268794,2497899128,2784771155,503983604,4076293799,907881277,423175695,432175456,1378068232,4145222326,3954048622,3938656102,
+3820766613,2793130115,2977904593,26017576,3274890735,3194772133,1700274565,1756076034,4006520079,3677328699,720338349,1533947780,354530856,688349552,3973924725,1637815568,332179504,3949051286,53804574,2852348879,3044236432,1282449977,3583942155,3416972820,4006381244,1617046695,2628476075,3002303598,1686838959,431878346,2686675385,1700445008,1080580658,1009431731,832498133,3223435511,2605976345,2271191193,2516031870,1648197032,4164389018,2548247927,300782431,375919233,238389289,3353747414,2531188641,
+2019080857,1475708069,455242339,2609103871,448939670,3451063019,1395535956,2413381860,1841049896,1491858159,885456874,4264095073,4001119347,1565136089,3898914787,1108368660,540939232,1173283510,2745871338,3681308437,4207628240,3343053890,4016749493,1699691293,1103962373,3625875870,2256883143,3830138730,1031889488,3479347698,1535977030,4236805024,3251091107,2132092099,1774941330,1199868427,1452454533,157007616,2904115357,342012276,595725824,1480756522,206960106,497939518,591360097,863170706,2375253569,
+3596610801,1814182875,2094937945,3421402208,1082520231,3463918190,2785509508,435703966,3908032597,1641649973,2842273706,3305899714,1510255612,2148256476,2655287854,3276092548,4258621189,236887753,3681803219,274041037,1734335097,3815195456,3317970021,1899903192,1026095262,4050517792,356393447,2410691914,3873677099,3682840055,3913112168,2491498743,4132185628,2489919796,1091903735,1979897079,3170134830,3567386728,3557303409,857797738,1136121015,1342202287,507115054,2535736646,337727348,3213592640,1301675037,
+2528481711,1895095763,1721773893,3216771564,62756741,2142006736,835421444,2531993523,1442658625,3659876326,2882144922,676362277,1392781812,170690266,3921047035,1759253602,3611846912,1745797284,664899054,1329594018,3901205900,3045908486,2062866102,2865634940,3543621612,3464012697,1080764994,553557557,3656615353,3996768171,991055499,499776247,1265440854,648242737,3940784050,980351604,3713745714,1749149687,3396870395,4211799374,3640570775,1161844396,3125318951,1431517754,545492359,4268468663,3499529547,
+1437099964,2702547544,3433638243,2581715763,2787789398,1060185593,1593081372,2418618748,4260947970,69676912,2159744348,86519011,2512459080,3838209314,1220612927,3339683548,133810670,1090789135,1078426020,1569222167,845107691,3583754449,4072456591,1091646820,628848692,1613405280,3757631651,526609435,236106946,48312990,2942717905,3402727701,1797494240,859738849,992217954,4005476642,2243076622,3870952857,3732016268,765654824,3490871365,2511836413,1685915746,3888969200,1414112111,2273134842,3281911079,
+4080962846,172450625,2569994100,980381355,4109958455,2819808352,2716589560,2568741196,3681446669,3329971472,1835478071,660984891,3704678404,4045999559,3422617507,3040415634,1762651403,1719377915,3470491036,2693910283,3642056355,3138596744,1364962596,2073328063,1983633131,926494387,3423689081,2150032023,4096667949,1749200295,3328846651,309677260,2016342300,1779581495,3079819751,111262694,1274766160,443224088,298511866,1025883608,3806446537,1145181785,168956806,3641502830,3584813610,1689216846,3666258015,
+3200248200,1692713982,2646376535,4042768518,1618508792,1610833997,3523052358,4130873264,2001055236,3610705100,2202168115,4028541809,2961195399,1006657119,2006996926,3186142756,1430667929,3210227297,1314452623,4074634658,4101304120,2273951170,1399257539,3367210612,3027628629,1190975929,2062231137,2333990788,2221543033,2438960610,1181637006,548689776,2362791313,3372408396,3104550113,3145860560,296247880,1970579870,3078560182,3769228297,1714227617,3291629107,3898220290,166772364,1251581989,493813264,
+448347421,195405023,2709975567,677966185,3703036547,1463355134,2715995803,1338867538,1343315457,2802222074,2684532164,233230375,2599980071,2000651841,3277868038,1638401717,4028070440,3237316320,6314154,819756386,300326615,590932579,1405279636,3267499572,3150704214,2428286686,3959192993,3461946742,1862657033,1266418056,963775037,2089974820,2263052895,1917689273,448879540,3550394620,3981727096,150775221,3627908307,1303187396,508620638,2975983352,2726630617,1817252668,1876281319,1457606340,908771278,
+3720792119,3617206836,2455994898,1729034894,1080033504,976866871,3556439503,2881648439,1522871579,1555064734,1336096578,3548522304,2579274686,3574697629,3205460757,3593280638,3338716283,3079412587,564236357,2993598910,1781952180,1464380207,3163844217,3332601554,1699332808,1393555694,1183702653,3581086237,1288719814,691649499,2847557200,2895455976,3193889540,2717570544,1781354906,1676643554,2592534050,3230253752,1126444790,2770207658,2633158820,2210423226,2615765581,2414155088,3127139286,673620729,
+2805611233,1269405062,4015350505,3341807571,4149409754,1057255273,2012875353,2162469141,2276492801,2601117357,993977747,3918593370,2654263191,753973209,36408145,2530585658,25011837,3520020182,2088578344,530523599,2918365339,1524020338,1518925132,3760827505,3759777254,1202760957,3985898139,3906192525,674977740,4174734889,2031300136,2019492241,3983892565,4153806404,3822280332,352677332,2297720250,60907813,90501309,3286998549,1016092578,2535922412,2839152426,457141659,509813237,4120667899,652014361,
+1966332200,2975202805,55981186,2327461051,676427537,3255491064,2882294119,3433927263,1307055953,942726286,933058658,2468411793,3933900994,4215176142,1361170020,2001714738,2830558078,3274259782,1222529897,1679025792,2729314320,3714953764,1770335741,151462246,3013232138,1682292957,1483529935,471910574,1539241949,458788160,3436315007,1807016891,3718408830,978976581,1043663428,3165965781,1927990952,4200891579,2372276910,3208408903,3533431907,1412390302,2931980059,4132332400,1947078029,3881505623,4168226417,
+2941484381,1077988104,1320477388,886195818,18198404,3786409E3,2509781533,112762804,3463356488,1866414978,891333506,18488651,661792760,1628790961,3885187036,3141171499,876946877,2693282273,1372485963,791857591,2686433993,3759982718,3167212022,3472953795,2716379847,445679433,3561995674,3504004811,3574258232,54117162,3331405415,2381918588,3769707343,4154350007,1140177722,4074052095,668550556,3214352940,367459370,261225585,2610173221,4209349473,3468074219,3265815641,314222801,3066103646,3808782860,282218597,
+3406013506,3773591054,379116347,1285071038,846784868,2669647154,3771962079,3550491691,2305946142,453669953,1268987020,3317592352,3279303384,3744833421,2610507566,3859509063,266596637,3847019092,517658769,3462560207,3443424879,370717030,4247526661,2224018117,4143653529,4112773975,2788324899,2477274417,1456262402,2901442914,1517677493,1846949527,2295493580,3734397586,2176403920,1280348187,1908823572,3871786941,846861322,1172426758,3287448474,3383383037,1655181056,3139813346,901632758,1897031941,2986607138,
+3066810236,3447102507,1393639104,373351379,950779232,625454576,3124240540,4148612726,2007998917,544563296,2244738638,2330496472,2058025392,1291430526,424198748,50039436,29584100,3605783033,2429876329,2791104160,1057563949,3255363231,3075367218,3463963227,1469046755,985887462],B=[1332899944,1700884034,1701343084,1684370003,1668446532,1869963892],x=q;m.setRandomPolyfill=function(c){x=c};m.genSaltSync=function(c){c||(c=10);var a;c=c||10;(4>c||31<c)&&p(Error("Illegal number of rounds: "+c));var b=[];
+b.push("$2a$");10>c&&b.push("0");b.push(c.toString());b.push("$");try{b.push(v.b(G(),16)),a=b.join("")}catch(f){p(f)}return a};m.genSalt=function(c,a,b){"function"==typeof a&&(b=a,a=-1);var f;"function"==typeof c?(b=c,f=10):f=parseInt(c,10);"function"!=typeof b&&p(Error("Illegal or missing 'callback': "+b));z(function(){try{var a=m.genSaltSync(f);b(q,a)}catch(c){b(c,q)}})};m.hashSync=function(c,a){a||(a=10);"number"==typeof a&&(a=m.genSaltSync(a));return w(c,a)};m.hash=function(c,a,b){"function"!=
+typeof b&&p(Error("Illegal 'callback': "+b));"number"==typeof a?m.genSalt(a,function(a,d){w(c,d,b)}):w(c,a,b)};m.compareSync=function(c,a){("string"!=typeof c||"string"!=typeof a)&&p(Error("Illegal argument types: "+typeof c+", "+typeof a));60!=a.length&&p(Error("Illegal hash length: "+a.length+" != 60"));for(var b=m.hashSync(c,a.substr(0,a.length-31)),f=b.length==a.length,d=b.length<a.length?b.length:a.length,e=0;e<d;++e)b.length>=e&&(a.length>=e&&b[e]!=a[e])&&(f=!1);return f};m.compare=function(c,
+a,b){"function"!=typeof b&&p(Error("Illegal 'callback': "+b));m.hash(c,a.substr(0,29),function(c,d){b(c,a===d)})};m.getRounds=function(c){"string"!=typeof c&&p(Error("Illegal type of 'hash': "+typeof c));return parseInt(c.split("$")[2],10)};m.getSalt=function(c){"string"!=typeof c&&p(Error("Illegal type of 'hash': "+typeof c));60!=c.length&&p(Error("Illegal hash length: "+c.length+" != 60"));return c.substring(0,29)};"undefined"!=typeof module&&module.exports?module.exports=m:"undefined"!=typeof define&&
+define.amd?define("bcrypt",function(){return m}):(n.dcodeIO||(n.dcodeIO={}),n.dcodeIO.bcrypt=m)})(this);

+ 15 - 2
p/scripts/global_view.js

@@ -24,6 +24,16 @@ function load_panel(link) {
 		// en en ouvrant une autre ensuite, on se retrouve au même point de scroll
 		$("#panel").scrollTop(0);
 
+		$('#panel').on('click', '#nav_menu_read_all > a, #nav_menu_read_all .item > a, #bigMarkAsRead', function () {
+			$.ajax({
+				url: $(this).attr("href"),
+				async: false
+			});
+			//$("#panel .close").first().click();
+			window.location.reload(false);
+			return false;
+		});
+
 		panel_loading = false;
 	});
 }
@@ -50,11 +60,14 @@ function init_global_view() {
 
 	$(".nav_menu #nav_menu_read_all, .nav_menu .toggle_aside").remove();
 
-	init_stream_delegates($("#panel"));
+	init_stream($("#panel"));
 }
 
 function init_all_global_view() {
-	if (!(window.$ && window.init_stream_delegates)) {
+	if (!(window.$ && window.init_stream)) {
+		if (window.console) {
+			console.log('FreshRSS Global view waiting for JS…');
+		}
 		window.setTimeout(init_all_global_view, 50);	//Wait for all js to be loaded
 		return;
 	}

+ 169 - 97
p/scripts/main.js

@@ -25,8 +25,52 @@ function incLabel(p, inc) {
 	return i > 0 ? ' (' + i + ')' : '';
 }
 
+function incUnreadsFeed(article, feed_id, nb) {
+	//Update unread: feed
+	var elem = $('#' + feed_id + '>.feed').get(0),
+		feed_unreads = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0,
+		feed_priority = elem ? (parseInt(elem.getAttribute('data-priority'), 10) || 0) : 0;
+	if (elem) {
+		elem.setAttribute('data-unread', Math.max(0, feed_unreads + nb));
+	}
+
+	//Update unread: category
+	elem = $('#' + feed_id).parent().prevAll('.category').children(':first').get(0);
+	feed_unreads = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
+	if (elem) {
+		elem.setAttribute('data-unread', Math.max(0, feed_unreads + nb));
+	}
+
+	//Update unread: all
+	if (feed_priority > 0) {
+		elem = $('#aside_flux .all').children(':first').get(0);
+		if (elem) {
+			feed_unreads = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
+			elem.setAttribute('data-unread', Math.max(0, feed_unreads + nb));
+		}
+	}
+
+	//Update unread: favourites
+	if (article && article.closest('div').hasClass('favorite')) {
+		elem = $('#aside_flux .favorites').children(':first').get(0);
+		if (elem) {
+			feed_unreads = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
+			elem.setAttribute('data-unread', Math.max(0, feed_unreads + nb));
+		}
+	}
+
+	//Update unread: title
+	document.title = document.title.replace(/((?: \(\d+\))?)( · .*?)((?: \(\d+\))?)$/, function (m, p1, p2, p3) {
+		if (article || ($('#' + feed_id).closest('.active').length > 0)) {
+			return incLabel(p1, nb) + p2 + incLabel(p3, feed_priority > 0 ? nb : 0);
+		} else {
+			return p1 + p2 + incLabel(p3, feed_priority > 0 ? nb : 0);
+		}
+	});
+}
+
 function mark_read(active, only_not_read) {
-	if (active[0] === undefined || (only_not_read === true && !active.hasClass("not_read"))) {
+	if (active.length === 0 || (only_not_read === true && !active.hasClass("not_read"))) {
 		return false;
 	}
 
@@ -51,50 +95,14 @@ function mark_read(active, only_not_read) {
 		}
 		$r.find('.icon').replaceWith(data.icon);
 
-		//Update unread: feed
 		var feed_url = active.find(".website>a").attr("href"),
-			feed_id = feed_url.substr(feed_url.lastIndexOf('f_')),
-			elem = $('#' + feed_id + ' .feed').get(0),
-			feed_unread = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0,
-			feed_priority = elem ? (parseInt(elem.getAttribute('data-priority'), 10) || 0) : 0;
-		if (elem) {
-			elem.setAttribute('data-unread', Math.max(0, feed_unread + inc));
-		}
-
-		//Update unread: category
-		elem = $('#' + feed_id).parent().prevAll('.category').children(':first').get(0);
-		feed_unread = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
-		if (elem) {
-			elem.setAttribute('data-unread', Math.max(0, feed_unread + inc));
-		}
-
-		//Update unread: all
-		if (feed_priority > 0) {
-			elem = $('#aside_flux .all').children(':first').get(0);
-			if (elem) {
-				feed_unread = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
-				elem.setAttribute('data-unread', Math.max(0, feed_unread + inc));
-			}
-		}
-
-		//Update unread: favourites
-		if (active.closest('div').hasClass('favorite')) {
-			elem = $('#aside_flux .favorites').children(':first').get(0);
-			if (elem) {
-				feed_unread = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
-				elem.setAttribute('data-unread', Math.max(0, feed_unread + inc));
-			}
-		}
-
-		//Update unread: title
-		document.title = document.title.replace(/((?: \(\d+\))?)( - .*?)((?: \(\d+\))?)$/, function (m, p1, p2, p3) {
-			return incLabel(p1, inc) + p2 + incLabel(p3, feed_priority > 0 ? inc : 0);
-		});
+			feed_id = feed_url.substr(feed_url.lastIndexOf('f_'));
+		incUnreadsFeed(active, feed_id, inc);
 	});
 }
 
 function mark_favorite(active) {
-	if (active[0] === undefined) {
+	if (active.length === 0) {
 		return false;
 	}
 
@@ -128,13 +136,21 @@ function mark_favorite(active) {
 
 		if (active.closest('div').hasClass('not_read')) {
 			var elem = $('#aside_flux .favorites').children(':first').get(0),
-				feed_unread = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
-			elem.setAttribute('data-unread', Math.max(0, feed_unread + inc));
+				feed_unreads = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
+			if (elem) {
+				elem.setAttribute('data-unread', Math.max(0, feed_unreads + inc));
+			}
 		}
 	});
 }
 
 function toggleContent(new_active, old_active) {
+	old_active.removeClass("active").removeClass("current");
+
+	if (new_active.length === 0) {
+		return;
+	}
+
 	if (does_lazyload) {
 		new_active.find('img[data-original], iframe[data-original]').each(function () {
 			this.setAttribute('src', this.getAttribute('data-original'));
@@ -142,7 +158,6 @@ function toggleContent(new_active, old_active) {
 		});
 	}
 
-	old_active.removeClass("active").removeClass("current");
 	if (old_active[0] !== new_active[0]) {
 		if (isCollapsed) {
 			new_active.addClass("active");
@@ -188,30 +203,20 @@ function toggleContent(new_active, old_active) {
 
 function prev_entry() {
 	var old_active = $(".flux.current"),
-		last_active = $(".flux:last"),
-		new_active = old_active.prevAll(".flux:first");
-
-	if (new_active.hasClass("flux")) {
-		toggleContent(new_active, old_active);
-	} else if (old_active[0] === undefined && new_active[0] === undefined) {
-		toggleContent(last_active, old_active);
-	}
+		new_active = old_active.length === 0 ? $(".flux:last") : old_active.prevAll(".flux:first");
+	toggleContent(new_active, old_active);
 }
 
 function next_entry() {
 	var old_active = $(".flux.current"),
-		first_active = $(".flux:first"),
-		last_active = $(".flux:last"),
-		new_active = old_active.nextAll(".flux:first");
+		new_active = old_active.length === 0 ? $(".flux:first") : old_active.nextAll(".flux:first");
+	toggleContent(new_active, old_active);
 
-	if (new_active.hasClass("flux")) {
-		toggleContent(new_active, old_active);
-	} else if (old_active[0] === undefined && new_active[0] === undefined) {
-		toggleContent(first_active, old_active);
-	}
-
-	if ((!auto_load_more) && (last_active.attr("id") === new_active.attr("id"))) {
-		load_more_posts();
+	if (!auto_load_more) {
+		var last_active = $(".flux:last");
+		if (last_active.attr("id") === new_active.attr("id")) {
+			load_more_posts();
+		}
 	}
 }
 
@@ -308,6 +313,14 @@ function init_column_categories() {
 		$(this).parent().next(".feeds").slideToggle();
 		return false;
 	});
+	$('#aside_flux').on('click', '.feeds .dropdown-toggle', function () {
+		if ($(this).nextAll('.dropdown-menu').length === 0) {
+			var feed_id = $(this).closest('li').attr('id').substr(2),
+				feed_web = $(this).data('fweb'),
+				template = $('#feed_config_template').html().replace(/!!!!!!/g, feed_id).replace('http://example.net/', feed_web);
+			$(this).attr('href', '#dropdown-' + feed_id).prev('.dropdown-target').attr('id', 'dropdown-' + feed_id).parent().append(template);
+		}
+	});
 }
 
 function init_shortcuts() {
@@ -399,8 +412,11 @@ function init_shortcuts() {
 	});
 }
 
-function init_stream_delegates(divStream) {
+function init_stream(divStream) {
 	divStream.on('click', '.flux_header', function (e) {	//flux_header_toggle
+		if ($(e.target).closest('.item.website > a').length > 0) {
+			return;
+		}
 		var old_active = $(".flux.current"),
 			new_active = $(this).parent();
 			isCollapsed = true;
@@ -416,21 +432,15 @@ function init_stream_delegates(divStream) {
 	divStream.on('click', '.flux a.read', function () {
 		var active = $(this).parents(".flux");
 		mark_read(active, false);
-
 		return false;
 	});
 
 	divStream.on('click', '.flux a.bookmark', function () {
 		var active = $(this).parents(".flux");
 		mark_favorite(active);
-
 		return false;
 	});
 
-	divStream.on('click', '.flux .content a', function () {
-		$(this).attr('target', '_blank');
-	});
-
 	divStream.on('click', '.item.title>a', function (e) {
 		if (e.ctrlKey) {
 			return true;	//Allow default control-click behaviour such as open in backround-tab
@@ -439,6 +449,10 @@ function init_stream_delegates(divStream) {
 		return false;
 	});
 
+	divStream.on('click', '.flux .content a', function () {
+		$(this).attr('target', '_blank');
+	});
+
 	if (auto_mark_site) {
 		divStream.on('click', '.flux .link a', function () {
 			mark_read($(this).parent().parent().parent(), true);
@@ -470,17 +484,6 @@ function init_nav_entries() {
 	});
 }
 
-function init_templates() {
-	$('#aside_flux').on('click', '.feeds .dropdown-toggle', function () {
-		if ($(this).nextAll('.dropdown-menu').length === 0) {
-			var feed_id = $(this).closest('li').attr('id').substr(2),
-				feed_web = $(this).data('fweb'),
-				template = $('#feed_config_template').html().replace(/!!!!!!/g, feed_id).replace('http://example.net/', feed_web);
-			$(this).attr('href', '#dropdown-' + feed_id).prev('.dropdown-target').attr('id', 'dropdown-' + feed_id).parent().append(template);
-		}
-	});
-}
-
 function init_actualize() {
 	$("#actualize").click(function () {
 		$.getScript('./?c=javascript&a=actualize').done(function () {
@@ -504,7 +507,7 @@ function closeNotification() {
 
 function init_notifications() {
 	var notif = $(".notification");
-	if (notif[0] !== undefined) {
+	if (notif.length > 0) {
 		window.setInterval(closeNotification, 4000);
 
 		notif.find("a.close").click(function () {
@@ -514,6 +517,17 @@ function init_notifications() {
 	}
 }
 
+function refreshUnreads() {
+	$.getJSON('./?c=javascript&a=nbUnreadsPerFeed').done(function (data) {
+		$.each(data, function(feed_id, nbUnreads) {
+			feed_id = 'f_' + feed_id;
+			var elem = $('#' + feed_id + '>.feed').get(0),
+				feed_unreads = elem ? (parseInt(elem.getAttribute('data-unread'), 10) || 0) : 0;
+			incUnreadsFeed(null, feed_id, nbUnreads - feed_unreads);
+		});
+	});
+}
+
 //<endless_mode>
 var url_load_more = "",
 	load_more = false,
@@ -547,6 +561,8 @@ function load_more_posts() {
 }
 
 function init_load_more(box) {
+	box_load_more = box;
+
 	var $next_link = $("#load_more");
 	if (!$next_link.length) {
 		// no more article to load
@@ -554,8 +570,6 @@ function init_load_more(box) {
 		return;
 	}
 
-	box_load_more = box;
-
 	url_load_more = $next_link.attr("href");
 	var $prefetch = $('#prefetch');
 	if ($prefetch.attr('href') !== url_load_more) {
@@ -571,6 +585,58 @@ function init_load_more(box) {
 }
 //</endless_mode>
 
+//<Web login form>
+function poormanSalt() {	//If crypto.getRandomValues is not available
+	var text = '$2a$04$',
+		base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789/abcdefghijklmnopqrstuvwxyz';
+	for (var i = 22; i > 0; i--) {
+		text += base.charAt(Math.floor(Math.random() * 64));
+	}
+	return text;
+}
+
+function init_loginForm() {
+	var $loginForm = $('#loginForm');
+	if ($loginForm.length === 0) {
+		return;
+	}
+	if (!(window.dcodeIO)) {
+		if (window.console) {
+			console.log('FreshRSS waiting for bcrypt.js…');
+		}
+		window.setTimeout(init_loginForm, 100);
+		return;
+	}
+	$loginForm.on('submit', function() {
+		$('#loginButton').attr('disabled', '');
+		var success = false;
+		$.ajax({
+			url: './?c=javascript&a=nonce&user=' + $('#username').val(),
+			dataType: 'json',
+			async: false
+		}).done(function (data) {
+			if (data.salt1 == '' || data.nonce == '') {
+				alert('Invalid user!');
+			} else {
+				var strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function'),
+					s = dcodeIO.bcrypt.hashSync($('#passwordPlain').val(), data.salt1),
+					c = dcodeIO.bcrypt.hashSync(data.nonce + s, strong ? 4 : poormanSalt());
+				$('#challenge').val(c);
+				if (s == '' || c == '') {
+					alert('Crypto error!');
+				} else {
+					success = true;
+				}
+			}
+		}).fail(function() {
+			alert('Communication error!');
+		});
+		$('#loginButton').removeAttr('disabled');
+		return success;
+	});
+}
+//</Web login form>
+
 //<persona>
 function init_persona() {
 	if (!(navigator.id)) {
@@ -668,23 +734,29 @@ function init_all() {
 		window.setTimeout(init_all, 50);
 		return;
 	}
-	$stream = $('#stream');
-	init_posts();
-	init_column_categories();
-	if (load_shortcuts) {
-		init_shortcuts();
-	}
-	init_stream_delegates($stream);
-	init_nav_entries();
-	init_templates();
 	init_notifications();
-	init_actualize();
-	init_load_more($stream);
-	if (use_persona) {
-		init_persona();
+	switch (authType) {
+		case 'form':
+			init_loginForm();
+			break;
+		case 'persona':
+			init_persona();
+			break;
 	}
 	init_confirm_action();
-	init_print_action();
+	$stream = $('#stream');
+	if ($stream.length > 0) {
+		init_actualize();
+		init_column_categories();
+		init_load_more($stream);
+		init_posts();
+		init_stream($stream);
+		init_nav_entries();
+		init_shortcuts();
+		init_print_action();
+		window.setInterval(refreshUnreads, 120000);
+	}
+
 	if (window.console) {
 		console.log('FreshRSS init done.');
 	}

+ 862 - 0
p/themes/Dark/freshrss.css

@@ -0,0 +1,862 @@
+@charset "UTF-8";
+
+/* STRUCTURE */
+.header {
+	display: table;
+	width: 100%;
+	background: #1c1c1c;
+	table-layout: fixed;
+}
+	.header > .item {
+		display: table-cell;
+		padding: 10px 0;
+		border-bottom: 1px solid #2f2f2f;
+		vertical-align: middle;
+		text-align: center;
+	}
+		.header > .item.title {
+			width: 250px;
+			white-space: nowrap;
+		}
+			.logo {
+				display: inline-block;
+				font-size: 48px;
+				height: 32px;
+				width: 32px;
+				padding: 10px;
+			}
+			.header > .item.title h1 {
+				display: inline-block;
+				margin: 0;
+			}
+		.header > .item.search input {
+			width: 230px;
+			transition: width 200ms linear;
+		}
+			.header .item.search input:focus {
+				width: 330px;
+			}
+		.header > .item.configure {
+			width: 100px;
+		}
+
+.item a:hover {
+	text-decoration: none;
+}
+
+#global {
+	display: table;
+	width: 100%;
+	height: 100%;
+	background: #1c1c1c;
+	table-layout: fixed;
+}
+	.aside {
+		display: table-cell;
+		height: 100%;
+		width: 250px;
+		vertical-align: top;
+		border-right: 1px solid #2f2f2f;
+		background: #1c1c1c;
+	}
+		.aside .nav-form input {
+			width: 180px;
+		}
+		.aside.aside_flux {
+			padding: 10px 0 40px;
+		}
+		.aside.aside_feed .nav-form input {
+			width: 140px;
+		}
+		.aside.aside_feed .nav-form .dropdown .dropdown-menu {
+			right: -20px;
+		}
+		.aside.aside_feed .nav-form .dropdown .dropdown-menu:after {
+			right: 33px;
+		}
+
+	.nav-login {
+		display: none;
+	}
+
+	.nav_menu {
+		width: 100%;
+		background: #1c1c1c;
+		border-bottom: 1px solid #2f2f2f;
+		text-align: center;
+		padding: 5px 0;
+	}
+		.nav_menu .search {
+			display:none;
+		}
+
+.favicon {
+	height: 16px;
+	width: 16px;
+}
+
+.categories {
+	margin: 0;
+	padding: 0;
+	text-align: center;
+	list-style: none;
+}
+	.category {
+		display: block;
+		width: 220px;
+		margin: 10px auto;
+		text-align: left;
+		overflow: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+	}
+		.category .btn:first-child {
+			width: 195px;
+			position: relative;
+		}
+		.category.stick .btn:first-child {
+			width:160px;
+		}
+		.category .btn:first-child:not([data-unread="0"]):after {
+			content: attr(data-unread);
+			position: absolute;
+			top: 3px; right: 3px;
+			padding: 1px 5px;
+			background: #1a1a1a;
+			color: #888;
+			font-size: 90%;
+			border: 1px solid #000;
+			border-radius: 5px;
+		}
+		.category + .feeds:not(.active) {
+			display:none;
+		}
+	.categories .feeds {
+		width: 100%;
+		margin: 0;
+		list-style: none;
+	}
+		.categories .feeds .item.active {
+			background: #26303F;
+		}
+		.categories .feeds .item.active .feed {
+			color: #888;
+		}
+		.categories .feeds .item.empty .feed {
+			color: #e67e22;
+		}
+		.categories .feeds .item.empty.active {
+			background: #e67e22;
+		}
+		.categories .feeds .item.empty.active .feed {
+			color: #fff;
+		}
+		.categories .feeds .item.error .feed {
+			color: #BD362F;
+		}
+		.categories .feeds .item .feed {
+			display: inline-block;
+			margin: 0;
+			width: 165px;
+			line-height: 35px;
+			font-size: 90%;
+			vertical-align: middle;
+			text-align: left;
+			overflow: hidden;
+			white-space: nowrap;
+			text-overflow: ellipsis;
+		}
+		.feed:not([data-unread="0"]):before {
+			content: "(" attr(data-unread) ") ";
+		}
+		.categories .feeds .dropdown-menu {
+			left: 0;
+		}
+		.categories .feeds .dropdown-menu:after {
+			left: 2px;
+		}
+		.categories .feeds .item .dropdown-toggle > .icon {
+			visibility: hidden;
+			cursor: pointer;
+		}
+			.categories .feeds .item .dropdown-target:target ~ .dropdown-toggle > .icon,
+			.categories .feeds .item:hover .dropdown-toggle > .icon,
+			.categories .feeds .item.active .dropdown-toggle > .icon {
+				background-color: #1c1c1c;
+				border-radius: 3px;
+				visibility: visible;
+			}
+
+.post {
+	padding: 10px 50px;
+}
+	.post form {
+		margin: 10px 0;
+	}
+
+.day {
+	min-height: 50px;
+	padding: 0 10px;
+	font-size: 130%;
+	font-weight: bold;
+	line-height: 50px;
+	background: #1c1c1c;
+	border-top: 1px solid #888;
+	border-bottom: 1px solid #888;
+}
+	.day:first-child {
+		border-top: none;
+	}
+	.day .name {
+		position: absolute;
+		right: 0;
+		width: 50%;
+		height: 1.5em;
+		padding: 0 10px 0 0;
+		overflow: hidden;
+		color: #aab;
+		font-size: 1.8em;
+		opacity: .3;
+		text-shadow: 0px -1px 0px #333;
+		font-style: italic;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		text-align: right;
+	}
+
+.flux {
+	border-left: 3px solid #aaa;
+	background: #1c1c1c;
+}
+	.flux.not_read {
+		border-left: 3px solid #FF5300;
+		background: #1c1c1c;
+	}
+	.flux.favorite {
+		border-left: 3px solid #FFC300;
+		background: #1c1c1c;
+	}
+	.flux.current {
+		border-left: 3px solid #0062BE;
+		background: #1a1a1a;
+	}
+
+	.flux_header {
+		background: inherit;
+		height: 25px;
+		font-size: 12px;
+		border-top: 1px solid #2f2f2f;
+		cursor: pointer;
+	}
+		.flux .item {
+			line-height: 40px;
+			white-space: nowrap;
+		}
+		.flux_header > .item {
+			overflow: hidden;
+			text-overflow: ellipsis;
+		}
+		.flux .item.manage {
+			width: 40px;
+			text-align: center;
+		}
+		.flux .item.website {
+			width: 200px;
+		}
+			.website .favicon {
+				padding: 5px;
+			}
+		.flux .item.title {
+			background: inherit;
+		}
+		.flux:hover .item.title {
+			border-right: 2px solid rgba(127, 127, 127, 0.1);
+			padding-right: 1em;
+			position: absolute;
+		}
+			.flux .item.title a {
+				color: #888;
+				outline: none;
+			}
+			.flux.not_read .item.title,
+			.flux.current .item.title {
+				font-weight: bold;
+			}
+		.flux .item.date {
+			width: 200px;
+			padding:0 5px 0 0;
+			text-align: right;
+			font-size: 10px;
+			color: #666;
+		}
+		.link {
+			width: 40px;
+			text-align: center;
+		}
+
+#stream.reader .flux {
+	padding: 0 0 30px;
+	border: none;
+	background: #1c1c1c;
+	color: #888;
+}
+	#stream.reader .flux .author {
+		margin: 0 0 10px;
+		font-size: 90%;
+		color: #666;
+	}
+
+#stream.global {
+	text-align: center;
+}
+	#stream.global .box-category {
+		display: inline-block;
+		width: 280px;
+		margin: 20px 10px;
+		vertical-align: top;
+		background: #1a1a1a;
+		border: 1px solid #000;
+		border-radius: 5px;
+		text-align: left;
+		box-shadow: 0 0 5px #2f2f2f;
+	}
+		#stream.global .category {
+			width: 100%;
+			margin: 0;
+		}
+		#stream.global .btn {
+			display: block;
+			width: auto;
+			height: 35px;
+			margin: 0;
+			padding: 0 10px;
+			background: #26303F;
+			border: none;
+			border-bottom: 1px solid #2f2f2f;
+			border-radius: 5px 5px 0 0;
+			line-height: 35px;
+			font-size: 120%;
+		}
+			#stream.global .btn:not([data-unread="0"]) {
+				font-weight:bold;
+			}
+			#stream.global .btn:first-child:not([data-unread="0"]):after {
+				top: 0; right: 5px;
+				border: 0;
+				background: none;
+				color: #666;
+				font-weight: bold;
+				box-shadow: none;
+			}
+		#stream.global .box-category .feeds {
+			display: block;
+			max-height: 250px;
+			margin: 0;
+			list-style: none;
+			overflow: auto;
+		}
+			#stream.global .box-category .feeds .item {
+				padding: 2px 10px;
+				font-size: 90%;
+			}
+		#stream.global .box-category .feed {
+			width: 220px;
+		}
+
+.content {
+	min-height: 150px;
+	max-width: 550px;
+	margin: 0 auto;
+	padding: 20px 10px;
+	line-height: 170%;
+	word-wrap: break-word;
+}
+	.content h1, .content h2, .content h3 {
+		margin: 20px 0 5px;
+	}
+	.content > .title {
+		font-size: x-large;
+		margin: 0;
+	}
+
+	.content p {
+		margin: 0 0 20px;
+	}
+	img.big {
+		display: block;
+		margin: 10px auto;
+	}
+	figure img.big {
+		margin: 0;
+	}
+	.content hr {
+		margin: 30px 0;
+		height: 1px;
+		background: #ddd;
+		border: 0;
+	}
+	.content pre {
+		margin: 10px auto;
+		padding: 10px;
+		overflow: auto;
+		background: #000;
+		color: #fff;
+		font-size: 110%;
+	}
+	.content q, .content blockquote {
+		display: block;
+		margin: 5px 0;
+		padding: 5px 20px;
+		font-style: italic;
+		border-left: 4px solid #ccc;
+		color: #666;
+	}
+		.content blockquote p {
+			margin: 0;
+		}
+
+#panel {
+	display: none;
+	position: fixed;
+	top: 10px; bottom: 10px;
+	left: 100px; right: 100px;
+	overflow: auto;
+	background: #1c1c1c;
+	border: 1px solid #95a5a6;
+	border-radius: 5px;
+}
+	#panel .close {
+		position: fixed;
+		top: 10px; right: 0;
+		display: inline-block;
+		width: 26px;
+		height: 26px;
+		margin: 0 10px 0 0;
+		border: 1px solid #ccc;
+		border-radius: 20px;
+		text-align: center;
+		line-height: 26px;
+		background: #fff;
+	}
+
+#overlay {
+	display: none;
+	position: fixed;
+	top: 0; bottom: 0;
+	left: 0; right: 0;
+	background: rgba(0, 0, 0, 0.9);
+}
+
+.flux_content .bottom {
+	font-size: 90%;
+	text-align: center;
+}
+
+.hide_posts > :not(.active) > .flux_content {
+	display:none;
+}
+
+/*** PAGINATION ***/
+.pagination {
+	display: table;
+	width: 100%;
+	margin: 0;
+	background: #1a1a1a;
+	text-align: center;
+	color: #888;
+	font-size: 80%;
+	line-height: 200%;
+	table-layout: fixed;
+}
+	.pagination .item {
+		display: table-cell;
+		line-height: 40px;
+	}
+		.pagination .item.pager-current {
+			font-weight: bold;
+			font-size: 140%;
+		}
+		.pagination .pager-first,
+		.pagination .pager-previous,
+		.pagination .pager-next,
+		.pagination .pager-last {
+			width: 100px;
+		}
+		.pagination .item a {
+			display: block;
+			color: #333;
+			font-style: italic;
+		}
+	.pagination:first-child .item {
+		border-bottom: 1px solid #aaa;
+	}
+	.pagination:last-child .item {
+		border-top: 1px solid #aaa;
+	}
+
+#nav_entries {
+	display: table;
+	width: 250px;
+	height: 40px;
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	margin: 0;
+	background: #1c1c1c;
+	border-top: 1px solid #2f2f2f;
+	text-align: center;
+	line-height: 40px;
+	table-layout: fixed;
+}
+	#nav_entries .item {
+		display: table-cell;
+		width: 30%;
+	}
+		#nav_entries a {
+			display: block;
+		}
+		#nav_entries .i_up {
+			margin: 5px 0 0;
+			vertical-align: top;
+		}
+
+.loading {
+	background: url("loader.gif") center center no-repeat;
+	font-size: 0;
+}
+
+#bigMarkAsRead {
+	display: block;
+	font-style: normal;
+	padding: 32px 0 64px 0;
+	text-align: center;
+	text-decoration: none;
+}
+	#bigMarkAsRead:hover {
+		background: #1c1c1c;
+		color: #888;
+	}
+	.bigTick {
+		font-size: 72pt;
+		line-height: 1.6em;
+	}
+
+/*** NOTIFICATION ***/
+.notification {
+	position: absolute;
+	top: 10px;
+	left: 25%; right: 25%;
+	min-height: 30px;
+	padding: 10px;
+	line-height: 30px;
+	text-align: center;
+	border-radius: 5px;
+	box-shadow: 0 0 5px #666;
+	background: #1a1a1a;
+	color: #888;
+	font-weight: bold;
+	z-index: 10;
+}
+	.notification.good {
+		border:1px solid #f4f899;
+	}
+	.notification.bad {
+		border:1px solid #f4a899;
+	}
+	.notification a.close {
+		display: inline-block;
+		width: 16px;
+		height: 16px;
+		float: right;
+		margin: -20px -20px 0 0;
+		padding: 5px;
+		background: #1a1a1a;
+		border-radius: 50px;
+		line-height: 16px;
+	}
+	.notification.good a.close{
+		border:1px solid #f4f899;
+	}
+	.notification.bad a.close{
+		border:1px solid #f4a899;
+	}
+
+.toggle_aside, .btn.toggle_aside {
+	display: none;
+}
+
+.actualizeProgress {
+	position: fixed;
+	top: 10px;
+	left: 25%; right: 25%;
+	padding: 5px;
+	background: #1a1a1a;
+	text-align: center;
+	border: 1px solid #2f2f2f;
+	border-radius: 5px;
+}
+	.actualizeProgress progress {
+		max-width: 100%;
+		vertical-align: middle;
+	}
+	.actualizeProgress .progress {
+		color: #888;
+		font-size: 90%;
+		vertical-align: middle;
+	}
+
+.logs {
+	border: 1px solid #aaa;
+}
+	.log {
+		padding: 5px 2%;
+		overflow: auto;
+		background: #fafafa;
+		border-bottom: 1px solid #999;
+		color: #333;
+		font-size: 90%;
+	}
+		.log .date {
+			display: block;
+		}
+	.log.error {
+		background: #fdd;
+		color: #844;
+	}
+	.log.warning {
+		background: #ffe;
+		color: #c95;
+	}
+	.log.notice {
+		background: #f4f4f4;
+		color: #aaa;
+	}
+	.log.debug {
+		background: #111;
+		color: #eee;
+	}
+
+.form-group table {
+	border-collapse:collapse;
+	margin:10px 0 0 220px;
+	text-align:center;
+}
+
+.form-group tr, .form-group th, .form-group td {
+	border:1px solid #2f2f2f;
+	font-weight:normal;
+	padding:.5em;
+}
+
+select.number option {
+	text-align:right;
+}
+
+@media(max-width: 840px) {
+	.header,
+	.aside .btn-important,
+	.aside .feeds .dropdown,
+	.flux_header .item.website span,
+	.item.date {
+		display: none;
+	}
+	.flux_header .item.website {
+		width: 40px;
+		text-align: center;
+	}
+		.flux_header .item.website .favicon {
+			padding: 12px;
+		}
+
+	.nav-login {
+		display: block;
+	}
+
+	.content {
+		font-size: 120%;
+		padding: 0;
+	}
+
+	.pagination {
+		margin: 0 0 40px;
+	}
+	.pagination .pager-previous, .pagination .pager-next {
+		width: 100px;
+	}
+
+	.toggle_aside, .btn.toggle_aside {
+		display: inline-block;
+	}
+	.aside {
+		position: fixed;
+		top: 0; left: 0;
+		width: 0;
+		overflow: hidden;
+		border-right: none;
+		z-index: 10;
+		transition: width 200ms linear;
+	}
+		.aside.aside_flux {
+			padding: 10px 0 0;
+		}
+		.aside:target {
+			width: 80%;
+			border-right: 1px solid #aaa;
+			overflow: auto;
+		}
+		.aside .toggle_aside {
+			position: absolute;
+			right: 0;
+			display: inline-block;
+			width: 26px;
+			height: 26px;
+			margin: 0 10px 0 0;
+			border: 1px solid #ccc;
+			border-radius: 20px;
+			text-align: center;
+			line-height: 26px;
+		}
+		.aside .categories {
+			margin: 30px 0;
+		}
+
+	#nav_entries {
+		width: 100%;
+	}
+
+	.nav_menu .btn {
+		margin: 5px 10px;
+	}
+	.nav_menu .stick {
+		margin: 0 10px;
+	}
+	.nav_menu .stick .btn {
+		margin: 5px 0;
+	}
+	.nav_menu .search {
+			display: inline-block;
+			max-width: 97%;
+		}
+		.nav_menu .search input {
+			max-width: 97%;
+			width: 90px;
+		}
+		.nav_menu .search input:focus {
+			width: 400px;
+		}
+
+	#panel {
+		left: 5px; right: 5px;
+	}
+
+	.day .date {
+		display: none;
+	}
+	.day .name {
+		height: 2.6em;
+		font-size: 1em;
+		text-shadow: none;
+	}
+
+	.notification,
+	.actualizeProgress {
+		left: 10px;
+		right: 10px;
+	}
+}
+
+/*** FALLBACK ***/
+.btn {
+	background: #1c1c1c;
+}
+	.btn:hover {
+		background: -moz-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+		background: -webkit-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+		background: -o-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+		background: -ms-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+	}
+	.btn-important {
+		background: #26303F;
+	}
+		.btn-important:hover {
+			background: -moz-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+			background: -webkit-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+			background: -o-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+			background: -ms-linear-gradient(top, #4A5D7A 0%, #26303F 100%);
+		}
+	.btn-attention {
+		background: #880011;
+	}
+		.btn-attention:hover {
+			background: -moz-linear-gradient(top, #cc0044 0%, #880011 100%);
+			background: -webkit-linear-gradient(top, #cc0044 0%, #880011 100%);
+			background: -o-linear-gradient(top, #cc0044 0%, #880011 100%);
+			background: -ms-linear-gradient(top, #cc0044 0%, #880011 100%);
+		}
+
+.dropdown-menu:after {
+	-moz-transform: rotate(45deg);
+	-webkit-transform: rotate(45deg);
+	-ms-transform: rotate(45deg);
+}
+
+.nav-head {
+	background: #fff;
+	background: -moz-linear-gradient(top, #fff 0%, #f0f0f0 100%);
+	background: -webkit-linear-gradient(top, #fff 0%, #f0f0f0 100%);
+	background: -o-linear-gradient(top, #fff 0%, #f0f0f0 100%);
+	background: -ms-linear-gradient(top, #fff 0%, #f0f0f0 100%);
+}
+
+.header > .item.search input {
+	-moz-transition: width 200ms linear;
+	 -webkit-transition: width 200ms linear;
+	-o-transition: width 200ms linear;
+	-ms-transition: width 200ms linear;
+}
+
+@media(max-width: 840px) {
+	.aside {
+		-moz-transition: width 200ms linear;
+		 -webkit-transition: width 200ms linear;
+		-o-transition: width 200ms linear;
+		-ms-transition: width 200ms linear;
+	}
+}
+
+@media print {
+	.header,
+	.aside,
+	.nav_menu,
+	.day,
+	.flux_header,
+	.flux_content .bottom,
+	.pagination {
+		display: none;
+	}
+
+	html, body {
+		background: #fff;
+		color: #000;
+		font-family: Serif;
+		font-size: 12pt;
+	}
+
+	#global,
+	.flux_content {
+		display: block !important;
+	}
+
+	.flux_content .content {
+		width: 100% !important;
+		text-align: justify;
+	}
+
+	.flux_content .content a {
+		color: #000;
+	}
+	.flux_content .content a:after {
+		content: " (" attr(href) ") ";
+		text-decoration: underline;
+	}
+}

+ 491 - 0
p/themes/Dark/global.css

@@ -0,0 +1,491 @@
+@charset "UTF-8";
+
+/* FONTS */
+@font-face {
+	font-family: "OpenSans";
+	src: url("../fonts/openSans.woff") format("woff");
+}
+
+
+* {
+	margin: 0;
+	padding: 0;
+}
+html, body {
+	height: 100%;
+	font-size: 95%;
+	font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif;
+	color: #888;
+}
+
+/* LIENS */
+a {
+	color: #6986B2;
+	text-decoration: none;
+}
+	a:hover {
+		text-decoration: underline;
+	}
+
+/* LISTES */
+ul, ol, dl {
+	margin: 10px 0 10px 30px;
+	line-height: 190%;
+}
+	dd {
+		margin: 0 0 10px 30px;
+	}
+
+/* TITRES */
+h1, h2, h3 {
+	min-height: 40px;
+	margin: 15px 0 5px;
+	line-height: 40px;
+}
+
+/* IMG */
+figure {
+	margin: 5px 0 10px;
+	text-align: center;
+}
+	figcaption {
+		display: inline-block;
+		padding: 3px 20px;
+		color: #999;
+		font-style: italic;
+		border-bottom: 1px solid #ccc;
+	}
+img {
+	height: auto;
+	max-width: 100%;
+	vertical-align: middle;
+}
+	a img {
+		border: none;
+	}
+
+/* VIDEOS */
+iframe, embed, object, video {
+	max-width: 100%;
+}
+
+/* FORMULAIRES */
+legend {
+	display: block;
+	width: 100%;
+	margin: 20px 0 5px;
+	padding: 5px 0;
+	border-bottom: 1px solid #2f2f2f;
+	font-size: 150%;
+	clear: both;
+}
+label {
+	display: block;
+	min-height: 25px;
+	padding: 5px 0;
+	font-size: 14px;
+	line-height: 25px;
+	cursor: pointer;
+}
+input, select, textarea {
+	display: inline-block;
+	max-width: 100%;
+	min-height: 25px;
+	padding: 5px;
+	background: #333;
+	border: 1px solid #000;
+	border-radius: 3px;
+	color: #999;
+	line-height: 25px;
+	vertical-align: middle;
+	box-shadow: 0 2px 2px #1d1d1d inset;
+}
+	option {
+		padding:0 .5em 0 .5em;
+	}
+	input[type="radio"],
+	input[type="checkbox"] {
+		width: 15px !important;
+		min-height: 15px !important;
+	}
+	input:focus, select:focus, textarea:focus {
+		color: #6986b2;
+		border-color: #2f2f2f;
+	}
+	input:invalid, select:invalid {
+		border-color: red;
+		box-shadow: 0 0 2px 1px red;
+	}
+
+.form-group {
+	margin: 0;
+}
+	.form-group:after {
+		content: "";
+		display: block;
+		clear: both;
+	}
+	.form-group.form-actions {
+		min-width: 250px;
+		padding: 5px 0;
+		background: #1a1a1a;
+		border-top: 1px solid #2f2f2f;
+	}
+		.form-group.form-actions .btn {
+			margin: 0 10px;
+		}
+	.form-group .group-name {
+		display: block;
+		float: left;
+		width: 200px;
+		padding: 10px 0;
+		text-align: right;
+	}
+	.form-group .group-controls {
+		min-width: 250px;
+		min-height: 25px;
+		margin: 0 0 0 220px;
+		padding: 5px 0;
+	}
+		.form-group .group-controls .control {
+			display: block;
+			min-height: 30px;
+			padding: 5px 0;
+			line-height: 25px;
+			font-size: 14px;
+		}
+
+.stick {
+	display: inline-block;
+	white-space: nowrap;
+	font-size: 0px;
+	vertical-align: middle;
+}
+	.stick input,
+	.stick .btn {
+		border-radius: 0;
+		font-size: 14px;
+	}
+	.stick .btn:first-child,
+	.stick input:first-child {
+		border-radius: 3px 0 0 3px;
+	}
+		.stick .btn-important:first-child {
+			border-right: 1px solid #000;
+		}
+	.stick .btn:last-child,
+	.stick input:last-child {
+		border-radius: 0 3px 3px 0;
+	}
+	.stick .btn + .btn,
+	.stick .btn + input,
+	.stick input + .btn,
+	.stick input + input {
+		border-left: none;
+	}
+	.stick .btn + .dropdown > .btn {
+		border-left: none;
+		border-radius: 0 3px 3px 0;
+	}
+	.stick .btn + .dropdown a {
+		font-size: 12px;
+	}
+
+.btn {
+	display: inline-block;
+	min-height: 37px;
+	min-width: 15px;
+	padding: 5px 10px;
+	background: linear-gradient(to bottom, #fff 0%, #eee 100%);
+	border-radius: 3px;
+	border: 1px solid #000;
+	color: #888;
+	line-height: 20px;
+	vertical-align: middle;
+	cursor: pointer;
+	overflow: hidden;
+}
+	a.btn {
+		min-height: 25px;
+		line-height: 25px;
+	}
+	.btn:hover {
+		background: linear-gradient(to bottom, #4A5D7A, #26303F);
+		text-decoration: none;
+	}
+	.btn.active,
+	.btn:active,
+	.dropdown-target:target ~ .btn.dropdown-toggle {
+		background: #26303F;
+	}
+
+	.btn-important {
+		background: linear-gradient(to bottom, #0084CC, #0045CC);
+		color: #888888;
+		border: 1px solid #000000;
+	}
+		.btn-important:hover {
+			background: linear-gradient(to bottom, #0066CC, #0045CC);
+		}
+		.btn-important:active {
+			background: #0044CB;
+			box-shadow: none;
+		}
+
+	.btn-attention {
+		background: linear-gradient(to bottom, #E95B57, #BD362F);
+		color: #888888;
+		border: 1px solid #000000;
+	}
+		.btn-attention:hover {
+			background: linear-gradient(to bottom, #D14641, #BD362F);
+		}
+		.btn-attention:active {
+			background: #BD362F;
+			box-shadow: none;
+		}
+
+/* NAVIGATION */
+.nav-list .nav-header,
+.nav-list .item {
+	display: block;
+	height: 35px;
+	line-height: 35px;
+}
+	.nav-list .item:hover {
+		background: #1a1a1a;
+	}
+		.nav-list .item:hover a {
+			color: #26303F;
+		}
+	.nav-list .item.active {
+		background: #26303F;
+		color: #1a1a1a;
+	}
+		.nav-list .item.active a {
+			color: #888;
+		}
+	.nav-list .disable {
+		color: #aaa;
+		background: #fafafa;
+		text-align: center;
+	}
+	.nav-list .item > * {
+		display: block;
+		padding: 0 10px;
+		overflow: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+	}
+		.nav-list a:hover {
+			text-decoration: none;
+		}
+	.nav-list .item.error a {
+		color: #BD362F;
+	}
+		.nav-list .item.active.error a {
+			color: #fff;
+			background: #BD362F;
+		}
+	.nav-list .item.empty a {
+		color: #f39c12;
+	}
+		.nav-list .item.active.empty a {
+			color: #fff;
+			background: #f39c12;
+		}
+
+	.nav-list .nav-header {
+		padding: 0 10px;
+		background: #1a1a1a;
+		border-bottom: 1px solid #2f2f2f;
+		font-weight: bold;
+	}
+	.nav-list .separator {
+		display: block;
+		height: 0;
+		margin: 5px 0;
+		border-bottom: 1px solid #ddd;
+	}
+
+	.nav-list .nav-form {
+		padding: 3px;
+		text-align: center;
+	}
+
+.nav-head {
+	display: block;
+	margin: 0;
+	background: linear-gradient(to bottom, #fff, #f0f0f0);
+	border-bottom: 1px solid #ddd;
+	text-align: right;
+}
+	.nav-head .item {
+		display: inline-block;
+		padding: 5px 10px;
+	}
+
+/* HORIZONTAL-LIST */
+.horizontal-list {
+	display: table;
+	table-layout: fixed;
+	margin: 0;
+	padding: 0;
+	width: 100%;
+}
+	.horizontal-list .item {
+		display: table-cell;
+		vertical-align: middle;
+	}
+
+/* DROPDOWN */
+.dropdown {
+	position: relative;
+	display: inline-block;
+}
+	.dropdown-target {
+		display: none;
+	}
+
+	.dropdown-menu {
+		display: none;
+		min-width: 200px;
+		margin: 5px 0 0;
+		padding: 5px 0;
+		position: absolute;
+		right: 0px;
+		background: #1a1a1a;
+		border: 1px solid #888;
+		border-radius: 5px;
+		text-align: left;
+	}
+	.dropdown-menu:after {
+		content: "";
+		position: absolute;
+		top: -6px;
+		right: 13px;
+		width: 10px;
+		height: 10px;
+		background: #1a1a1a;
+		border-top: 1px solid #888;
+		border-left: 1px solid #888;
+		z-index: -10;
+		transform: rotate(45deg);
+	}
+		.dropdown-header {
+			display: block;
+			padding: 0 5px;
+			color: #888;
+			font-weight: bold;
+			font-size: 14px;
+			line-height: 30px;
+		}
+		.dropdown-menu .item {
+			display: block;
+			height: 30px;
+			font-size: 90%;
+			line-height: 30px;
+		}
+			.dropdown-menu > .item > a {
+				display: block;
+				padding: 0 25px;
+				line-height: 30px;
+			}
+			.dropdown-menu > .item:hover {
+				background: #26303F;
+				color: #888;
+			}
+				.dropdown-menu > .item[aria-checked="true"] > a:before {
+					content: '✓ ';
+					font-weight: bold;
+					margin: 0 0 0 -1.2em;
+					padding: 0 0.2em 0 0;
+				}
+				.dropdown-menu > .item:hover > a {
+					color: #888;
+					text-decoration: none;
+				}
+		.dropdown-menu .input {
+			display: block;
+			height: 40px;
+			font-size: 90%;
+			line-height: 30px;
+		}
+			.dropdown-menu .input select,
+			.dropdown-menu .input input {
+				display: block;
+				height: 20px;
+				width: 95%;
+				margin: auto;
+				padding: 2px 5px;
+				border-radius: 3px;
+			}
+			.dropdown-menu .input select {
+				width: 70%;
+				height: auto;
+			}
+		.dropdown-menu .separator {
+			display: block;
+			height: 0;
+			margin: 5px 0;
+			border-bottom: 1px solid #888;
+		}
+		.dropdown-target:target ~ .dropdown-menu {
+			display: block;
+			z-index: 10;
+		}
+	.dropdown-close {
+		display: inline;
+	}
+		.dropdown-close a {
+			font-size: 0;
+			position: fixed;
+			top: 0; bottom: 0;
+			left: 0; right: 0;
+			display: block;
+			z-index: -10;
+		}
+
+/* ALERTS */
+.alert {
+	display: block;
+	width: 90%;
+	margin: 15px auto;
+	padding: 10px 15px;
+	background: #1a1a1a;
+	border: 1px solid #ccc;
+	border-right: 1px solid #aaa;
+	border-bottom: 1px solid #aaa;
+	border-radius: 5px;
+	color: #aaa;
+}
+	.alert-head {
+		margin: 0;
+		font-weight: bold;
+		font-size: 110%;
+	}
+	.alert > a {
+		color: inherit;
+		text-decoration: underline;
+	}
+	.alert-warn {
+		border: 1px solid #c95;
+		color: #c95;
+	}
+	.alert-success {
+		border: 1px solid #484;
+		color: #484;
+	}
+	.alert-error {
+		border: 1px solid #844;
+		color: #844;
+	}
+
+/* ICÔNES */
+.icon {
+	display: inline-block;
+	width: 16px;
+	height: 16px;
+	vertical-align: middle;
+	line-height: 16px;
+}

BIN
p/themes/Dark/loader.gif


+ 7 - 0
p/themes/Dark/metadata.json

@@ -0,0 +1,7 @@
+{
+	"name": "Dark",
+	"author": "AD",
+	"description": "Le coté obscur du thème “Origine”",
+	"version": 0.1,
+	"files": ["global.css", "freshrss.css"]
+}

+ 7 - 17
p/themes/flat-design/freshrss.css → p/themes/Flat/freshrss.css

@@ -243,33 +243,26 @@ body {
 		border-top: 1px solid #ecf0f1;
 		cursor: pointer;
 	}
-		.flux .flux_header > .item > a,
-		.flux .bottom > .item > a {
-			display: inline-block;
-			height: 40px;
-			width: 100%;
+		.flux .item {
 			line-height: 40px;
+			white-space: nowrap;
+		}
+		.flux_header > .item {
+			overflow: hidden;
+			text-overflow: ellipsis;
 		}
 		.flux .item.manage {
 			width: 40px;
-			white-space: nowrap;
 			text-align: center;
 		}
 		.flux .item.website {
 			width: 200px;
-			overflow: hidden;
-			white-space: nowrap;
-			text-overflow: ellipsis;
-			line-height: 40px;
 		}
-			.flux .item.website .favicon {
+			.website .favicon {
 				padding: 5px;
 			}
 		.flux .item.title {
 			background: inherit;
-			overflow: hidden;
-			white-space: nowrap;
-			text-overflow: ellipsis;
 		}
 		.flux:hover .item.title {
 			border-right: 2px solid rgba(127, 127, 127, 0.1);
@@ -286,10 +279,7 @@ body {
 			}
 		.flux .item.date {
 			width: 200px;
-			overflow: hidden;
 			padding:0 5px 0 0;
-			white-space: nowrap;
-			text-overflow: ellipsis;
 			text-align: right;
 			font-size: 10px;
 			color: #666;

+ 14 - 4
p/themes/flat-design/global.css → p/themes/Flat/global.css

@@ -381,22 +381,28 @@ input, select, textarea {
 			font-size: 14px;
 			line-height: 30px;
 		}
-		.dropdown-menu .item {
+		.dropdown-menu > .item {
 			display: block;
 			height: 30px;
 			font-size: 90%;
 			line-height: 30px;
 		}
-			.dropdown-menu .item > * {
+			.dropdown-menu > .item > a {
 				display: block;
 				padding: 0 25px;
 				line-height: 30px;
 			}
-			.dropdown-menu .item:hover {
+			.dropdown-menu > .item:hover > a {
 				background: #2980b9;
 				color: #fff;
 			}
-				.dropdown-menu .item:hover > * {
+				.dropdown-menu > .item[aria-checked="true"] > a:before {
+					content: '✓ ';
+					font-weight: bold;
+					margin: 0 0 0 -1.2em;
+					padding: 0 0.2em 0 0;
+				}
+				.dropdown-menu > .item:hover > a {
 					color: #fff;
 					text-decoration: none;
 				}
@@ -463,6 +469,10 @@ input, select, textarea {
 		font-weight: bold;
 		font-size: 110%;
 	}
+	.alert > a {
+		color: inherit;
+		text-decoration: underline;
+	}
 	.alert-warn {
 		background: #ffe;
 		border: 1px solid #eeb;

+ 0 - 0
p/themes/flat-design/icons/add.svg → p/themes/Flat/icons/add.svg


+ 0 - 0
p/themes/flat-design/icons/all.svg → p/themes/Flat/icons/all.svg


+ 0 - 0
p/themes/flat-design/icons/close.svg → p/themes/Flat/icons/close.svg


+ 0 - 0
p/themes/flat-design/icons/configure.svg → p/themes/Flat/icons/configure.svg


+ 0 - 0
p/themes/flat-design/icons/down.svg → p/themes/Flat/icons/down.svg


+ 0 - 0
p/themes/flat-design/icons/next.svg → p/themes/Flat/icons/next.svg


+ 0 - 0
p/themes/flat-design/icons/prev.svg → p/themes/Flat/icons/prev.svg


+ 0 - 0
p/themes/flat-design/icons/refresh.svg → p/themes/Flat/icons/refresh.svg


+ 0 - 0
p/themes/flat-design/icons/search.svg → p/themes/Flat/icons/search.svg


+ 0 - 0
p/themes/flat-design/icons/up.svg → p/themes/Flat/icons/up.svg


+ 0 - 0
p/themes/flat-design/loader.gif → p/themes/Flat/loader.gif


+ 0 - 0
p/themes/flat-design/metadata.json → p/themes/Flat/metadata.json


+ 7 - 21
p/themes/default/freshrss.css → p/themes/Origine/freshrss.css

@@ -257,37 +257,26 @@
 		border-top: 1px solid #ddd;
 		cursor: pointer;
 	}
-		.flux .flux_header > .item > a,
-		.flux .bottom > .item > a {
-			display: inline-block;
-			height: 40px;
-			width: 100%;
+		.flux .item {
 			line-height: 40px;
+			white-space: nowrap;
+		}
+		.flux_header > .item {
+			overflow: hidden;
+			text-overflow: ellipsis;
 		}
 		.flux .item.manage {
 			width: 40px;
-			white-space: nowrap;
 			text-align: center;
 		}
 		.flux .item.website {
 			width: 200px;
-			overflow: hidden;
-			white-space: nowrap;
-			text-overflow: ellipsis;
-			line-height: 40px;
 		}
-			.flux .item.website .favicon {
+			.website .favicon {
 				padding: 5px;
 			}
-			.flux .item.website a {
-				display: block;
-				height: 40px;
-			}
 		.flux .item.title {
 			background: inherit;
-			overflow: hidden;
-			white-space: nowrap;
-			text-overflow: ellipsis;
 		}
 		.flux:hover .item.title {
 			border-right: 2px solid rgba(127, 127, 127, 0.1);
@@ -304,10 +293,7 @@
 			}
 		.flux .item.date {
 			width: 200px;
-			overflow: hidden;
 			padding:0 5px 0 0;
-			white-space: nowrap;
-			text-overflow: ellipsis;
 			text-align: right;
 			font-size: 10px;
 			color: #666;

+ 14 - 4
p/themes/default/global.css → p/themes/Origine/global.css

@@ -392,22 +392,28 @@ input, select, textarea {
 			font-size: 14px;
 			line-height: 30px;
 		}
-		.dropdown-menu .item {
+		.dropdown-menu > .item {
 			display: block;
 			height: 30px;
 			font-size: 90%;
 			line-height: 30px;
 		}
-			.dropdown-menu .item > * {
+			.dropdown-menu > .item > a {
 				display: block;
 				padding: 0 25px;
 				line-height: 30px;
 			}
-			.dropdown-menu .item:hover {
+			.dropdown-menu > .item:hover {
 				background: #0062BE;
 				color: #fff;
 			}
-				.dropdown-menu .item:hover > * {
+				.dropdown-menu > .item[aria-checked="true"] > a:before {
+					content: '✓ ';
+					font-weight: bold;
+					margin: 0 0 0 -1.2em;
+					padding: 0 0.2em 0 0;
+				}
+				.dropdown-menu > .item:hover > a {
 					color: #fff;
 					text-decoration: none;
 				}
@@ -471,6 +477,10 @@ input, select, textarea {
 		font-weight: bold;
 		font-size: 110%;
 	}
+	.alert > a {
+		color: inherit;
+		text-decoration: underline;
+	}
 	.alert-warn {
 		background: #ffe;
 		border: 1px solid #eeb;

+ 0 - 0
p/themes/default/loader.gif → p/themes/Origine/loader.gif


+ 1 - 1
p/themes/default/metadata.json → p/themes/Origine/metadata.json

@@ -1,5 +1,5 @@
 {
-  "name": "Default",
+  "name": "Origine",
   "author": "Marien Fressinaud",
   "description": "Le thème par défaut pour FreshRSS",
   "version": 0.1,