Răsfoiți Sursa

Merge branch 'dev' of https://github.com/marienfressinaud/FreshRSS into dev

plopoyop 11 ani în urmă
părinte
comite
ffbfbb92cc

+ 76 - 0
app/Controllers/indexController.php

@@ -83,6 +83,11 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		$nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page);
 		$first = Minz_Request::param ('next', '');
 
+		$ajax_request = Minz_Request::param('ajax', false);
+		if ($ajax_request == 1 && $this->view->conf->display_posts) {
+			$nb = max(1, round($nb / 2));
+		}
+
 		if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ) {	//Any unread article in this category at all?
 			switch ($getType) {
 				case 'a':
@@ -415,4 +420,75 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		self::deleteLongTermCookie();
 		Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
 	}
+
+	public function resetAuthAction() {
+		Minz_View::prependTitle(_t('auth_reset') . ' · ');
+		Minz_View::appendScript(Minz_Url::display(
+			'/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
+		));
+
+		$this->view->no_form = false;
+		// Enable changement of auth only if Persona!
+		if (Minz_Configuration::authType() != 'persona') {
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('auth_not_persona')
+			);
+			$this->view->no_form = true;
+			return;
+		}
+
+		$conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
+		// Admin user must have set its master password.
+		if (!$conf->passwordHash) {
+			$this->view->message = array(
+				'status' => 'bad',
+				'title' => _t('damn'),
+				'body' => _t('auth_no_password_set')
+			);
+			$this->view->no_form = true;
+			return;
+		}
+
+		invalidateHttpCache();
+
+		if (Minz_Request::isPost()) {
+			$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))) {
+				Minz_Log::debug('Invalid credential parameters:' .
+				                ' user=' . $username .
+				                ' challenge=' . $c .
+				                ' nonce=' . $nonce);
+				Minz_Request::bad(_t('invalid_login'),
+				                  array('c' => 'index', 'a' => 'resetAuth'));
+			}
+
+			if (!function_exists('password_verify')) {
+				include_once(LIB_PATH . '/password_compat.php');
+			}
+
+			$s = $conf->passwordHash;
+			$ok = password_verify($nonce . $s, $c);
+			if ($ok) {
+				Minz_Configuration::_authType('form');
+				$ok = Minz_Configuration::writeFile();
+
+				if ($ok) {
+					Minz_Request::good(_t('auth_form_set'));
+				} else {
+					Minz_Request::bad(_t('auth_form_not_set'),
+				                      array('c' => 'index', 'a' => 'resetAuth'));
+				}
+			} else {
+				Minz_Log::debug('Password mismatch for user ' . $username .
+				                ', nonce=' . $nonce . ', c=' . $c);
+
+				Minz_Request::bad(_t('invalid_login'),
+				                  array('c' => 'index', 'a' => 'resetAuth'));
+			}
+		}
+	}
 }

+ 5 - 1
app/Controllers/updateController.php

@@ -10,7 +10,10 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 			);
 		}
 
+		invalidateHttpCache();
+
 		Minz_View::prependTitle(_t('update_system') . ' · ');
+		$this->view->update_to_apply = false;
 		$this->view->last_update_time = 'unknown';
 		$this->view->check_last_hour = false;
 		$timestamp = (int)@file_get_contents(DATA_PATH . '/last_update.txt');
@@ -29,10 +32,11 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 			);
 		} elseif (file_exists(UPDATE_FILENAME)) {
 			// There is an update file to apply!
+			$this->view->update_to_apply = true;
 			$this->view->message = array(
 				'status' => 'good',
 				'title' => _t('ok'),
-				'body' => _t('update_can_apply', _url('update', 'apply'))
+				'body' => _t('update_can_apply')
 			);
 		}
 	}

+ 5 - 4
app/FreshRSS.php

@@ -6,7 +6,7 @@ class FreshRSS extends Minz_FrontController {
 		}
 		$loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
 		$this->loadParamsView();
-		if (Minz_Request::isPost() && !Minz_Request::isRefererFromSameDomain()) {
+		if (Minz_Request::isPost() && !is_referer_from_same_domain()) {
 			$loginOk = false;	//Basic protection against XSRF attacks
 			Minz_Error::error(
 				403,
@@ -143,11 +143,12 @@ class FreshRSS extends Minz_FrontController {
 		$theme = FreshRSS_Themes::load($this->conf->theme);
 		if ($theme) {
 			foreach($theme['files'] as $file) {
-				$theme_id = $theme['id'];
-				$filename = $file;
-				if ($file[0] == '_') {
+				if ($file[0] === '_') {
 					$theme_id = 'base-theme';
 					$filename = substr($file, 1);
+				} else {
+					$theme_id = $theme['id'];
+					$filename = $file;
 				}
 				$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
 				Minz_View::appendStyle(Minz_Url::display(

+ 12 - 1
app/Models/Configuration.php

@@ -142,7 +142,18 @@ class FreshRSS_Configuration {
 		}
 	}
 	public function _default_view ($value) {
-		$this->data['default_view'] = $value === FreshRSS_Entry::STATE_ALL ? FreshRSS_Entry::STATE_ALL : FreshRSS_Entry::STATE_NOT_READ;
+		switch ($value) {
+		case FreshRSS_Entry::STATE_ALL:
+			// left blank on purpose
+		case FreshRSS_Entry::STATE_NOT_READ:
+			// left blank on purpose
+		case FreshRSS_Entry::STATE_NOT_READ_STRICT:
+			$this->data['default_view'] = $value;
+			break;
+		default:
+			$this->data['default_view'] = FreshRSS_Entry::STATE_ALL;
+			break;
+		}
 	}
 	public function _display_posts ($value) {
 		$this->data['display_posts'] = ((bool)$value) && $value !== 'no';

+ 2 - 0
app/Models/Entry.php

@@ -6,6 +6,8 @@ class FreshRSS_Entry extends Minz_Model {
 	const STATE_NOT_READ = 2;
 	const STATE_FAVORITE = 4;
 	const STATE_NOT_FAVORITE = 8;
+	const STATE_READ_STRICT = 16;
+	const STATE_NOT_READ_STRICT = 32;
 
 	private $id = 0;
 	private $guid;

+ 3 - 0
app/Models/EntryDAO.php

@@ -338,6 +338,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		elseif ($state & FreshRSS_Entry::STATE_READ) {
 			$where .= 'AND e1.is_read=1 ';
 		}
+		elseif ($state & FreshRSS_Entry::STATE_NOT_READ_STRICT) {
+			$where .= 'AND e1.is_read=0 ';
+		}
 		if ($state & FreshRSS_Entry::STATE_FAVORITE) {
 			if (!($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
 				$where .= 'AND e1.is_favorite=1 ';

+ 1 - 1
app/Models/FeedDAO.php

@@ -331,7 +331,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
 		$id_max = intval($date_min) . '000000';
 
 		$stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
-		$stm->bindParam(':id_max', $id_max, PDO::PARAM_INT);
+		$stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
 		$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
 
 		if ($stm && $stm->execute()) {

+ 18 - 3
app/i18n/en.php

@@ -5,6 +5,7 @@ return array (
 	'login'				=> 'Login',
 	'keep_logged_in'		=> 'Keep me logged in <small>(1 month)</small>',
 	'login_with_persona'		=> 'Login with Persona',
+	'login_persona_problem'		=> 'Problem of connection with Persona?',
 	'logout'			=> 'Logout',
 	'search'			=> 'Search words or #tags',
 	'search_short'			=> 'Search',
@@ -92,6 +93,7 @@ return array (
 	'rss_view'			=> 'RSS feed',
 	'show_all_articles'		=> 'Show all articles',
 	'show_not_reads'		=> 'Show only unread',
+	'show_adaptive'			=> 'Adjust showing',
 	'show_read'			=> 'Show only read',
 	'show_favorite'			=> 'Show only favorites',
 	'show_not_favorite'		=> 'Show all but favorites',
@@ -158,6 +160,7 @@ return array (
 	'save'				=> 'Save',
 	'delete'			=> 'Delete',
 	'cancel'			=> 'Cancel',
+	'submit'			=> 'Submit',
 
 	'back_to_rss_feeds'		=> '← Go back to your RSS feeds',
 	'feeds_moved_category_deleted'	=> 'When you delete a category, their feeds are automatically classified under <em>%s</em>.',
@@ -203,6 +206,7 @@ return array (
 	'informations'			=> 'Information',
 	'damn'				=> 'Damn!',
 	'ok'				=> 'Ok!',
+	'attention'			=> 'Be careful!',
 	'feed_in_error'			=> 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
 	'feed_empty'			=> 'This feed is empty. Please verify that it is still maintained.',
 	'feed_description'		=> 'Description',
@@ -254,6 +258,7 @@ return array (
 	'users_list'			=> 'List of users',
 	'create_user'			=> 'Create new user',
 	'username'			=> 'Username',
+	'username_admin'		=> 'Administrator username',
 	'password'			=> 'Password',
 	'create'			=> 'Create',
 	'user_created'			=> 'User %s has been created',
@@ -269,7 +274,9 @@ return array (
 	'reading_configuration'		=> 'Reading',
 	'display_configuration'		=> 'Display',
 	'articles_per_page'		=> 'Number of articles per page',
+	'number_divided_when_unfolded'	=> 'Divided by 2 during loading of unfolded articles.',
 	'default_view'			=> 'Default view',
+	'articles_to_display'		=> 'Articles to display',
 	'sort_order'			=> 'Sort order',
 	'auto_load_more'		=> 'Load next articles at the page bottom',
 	'display_articles_unfolded'	=> 'Show articles unfolded by default',
@@ -427,9 +434,17 @@ return array (
 	'update_system'			=> 'Update system',
 	'update_check'			=> 'Check for new updates',
 	'update_last'			=> 'Last verification: %s',
-	'update_can_apply'		=> 'There is an available update. <a class="btn" href="%s">Apply</a>',
+	'update_can_apply'		=> 'An update is available.',
+	'update_apply'			=> 'Apply',
 	'update_server_not_found'	=> 'Update server cannot be found. [%s]',
 	'no_update'			=> 'No update to apply',
-	'update_problem'		=> 'Update has encountered an error: %s',
-	'update_finished'		=> 'Update is now  finished!',
+	'update_problem'		=> 'The update process has encountered an error: %s',
+	'update_finished'		=> 'Update completed!',
+
+	'auth_reset'			=> 'Authentication reset',
+	'auth_will_reset'		=> 'Authentication system will be reseted: form will be used instead of Persona.',
+	'auth_not_persona'		=> 'Only Persona system can be reseted.',
+	'auth_no_password_set'		=> 'Administrator password hasn’t been set. This feature isn’t available.',
+	'auth_form_set'			=> 'Form is now your default authentication system.',
+	'auth_form_not_set'		=> 'A problem occured during authentication system configuration. Please retry later.',
 );

+ 16 - 1
app/i18n/fr.php

@@ -5,6 +5,7 @@ return array (
 	'login'				=> 'Connexion',
 	'keep_logged_in'		=> 'Rester connecté <small>(1 mois)</small>',
 	'login_with_persona'		=> 'Connexion avec Persona',
+	'login_persona_problem'		=> 'Problème de connexion à Persona ?',
 	'logout'			=> 'Déconnexion',
 	'search'			=> 'Rechercher des mots ou des #tags',
 	'search_short'			=> 'Rechercher',
@@ -92,6 +93,7 @@ return array (
 	'rss_view'			=> 'Flux RSS',
 	'show_all_articles'		=> 'Afficher tous les articles',
 	'show_not_reads'		=> 'Afficher les non lus',
+	'show_adaptive'			=> 'Adapter l’affichage',
 	'show_read'			=> 'Afficher les lus',
 	'show_favorite'			=> 'Afficher les favoris',
 	'show_not_favorite'		=> 'Afficher tout sauf les favoris',
@@ -158,6 +160,7 @@ return array (
 	'save'				=> 'Enregistrer',
 	'delete'			=> 'Supprimer',
 	'cancel'			=> 'Annuler',
+	'submit'			=> 'Valider',
 
 	'back_to_rss_feeds'		=> '← Retour à vos flux RSS',
 	'feeds_moved_category_deleted'	=> 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
@@ -203,6 +206,7 @@ return array (
 	'informations'			=> 'Informations',
 	'damn'				=> 'Arf !',
 	'ok'				=> 'Ok !',
+	'attention'			=> 'Attention !',
 	'feed_in_error'			=> 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.',
 	'feed_empty'			=> 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.',
 	'feed_description'		=> 'Description',
@@ -254,6 +258,7 @@ return array (
 	'users_list'			=> 'Liste des utilisateurs',
 	'create_user'			=> 'Créer un nouvel utilisateur',
 	'username'			=> 'Nom d’utilisateur',
+	'username_admin'		=> 'Nom d’utilisateur administrateur',
 	'password'			=> 'Mot de passe',
 	'create'			=> 'Créer',
 	'user_created'			=> 'L’utilisateur %s a été créé.',
@@ -269,7 +274,9 @@ return array (
 	'reading_configuration'		=> 'Lecture',
 	'display_configuration'		=> 'Affichage',
 	'articles_per_page'		=> 'Nombre d’articles par page',
+	'number_divided_when_unfolded'	=> 'Divisé par 2 lors du chargement d’articles dépliés.',
 	'default_view'			=> 'Vue par défaut',
+	'articles_to_display'		=> 'Articles à afficher',
 	'sort_order'			=> 'Ordre de tri',
 	'auto_load_more'		=> 'Charger les articles suivants en bas de page',
 	'display_articles_unfolded'	=> 'Afficher les articles dépliés par défaut',
@@ -427,9 +434,17 @@ return array (
 	'update_system'			=> 'Système de mise à jour',
 	'update_check'			=> 'Vérifier les mises à jour',
 	'update_last'			=> 'Dernière vérification : %s',
-	'update_can_apply'		=> 'Il y’a une mise à jour à appliquer. <a class="btn" href="%s">Appliquer la mise à jour</a>',
+	'update_can_apply'		=> 'Une mise à jour est disponible.',
+	'update_apply'			=> 'Appliquer la mise à jour',
 	'update_server_not_found'	=> 'Le serveur de mise à jour n’a pas été trouvé. [%s]',
 	'no_update'			=> 'Aucune mise à jour à appliquer',
 	'update_problem'		=> 'La mise à jour a rencontré un problème : %s',
 	'update_finished'		=> 'La mise à jour est terminée !',
+
+	'auth_reset'			=> 'Reset de l’authentification',
+	'auth_will_reset'		=> 'Le système d’authentification va être remis à zéro : le formulaire sera utilisé à la place de Persona.',
+	'auth_not_persona'		=> 'Seul le système d’authentification Persona peut être remis à zéro.',
+	'auth_no_password_set'		=> 'Aucun mot de passe administrateur n’a été précisé. Cette fonctionnalité n’est pas disponible.',
+	'auth_form_set'			=> 'Le formulaire est désormais votre système d’authentification.',
+	'auth_form_not_set'		=> 'Un problème est survenu lors de la configuration de votre système d’authentification. Veuillez réessayer plus tard.',
 );

+ 2 - 0
app/i18n/install.en.php

@@ -42,6 +42,8 @@ return array (
 	'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',
+	'http_referer_is_ok'		=> 'Your HTTP REFERER is known and corresponds to your server.',
+	'http_referer_is_nok'		=> 'Please check that you are not altering your HTTP REFERER.',
 	'fix_errors_before'		=> 'Fix errors before skip to the next step.',
 
 	'general_conf_is_ok'		=> 'General configuration has been saved.',

+ 2 - 0
app/i18n/install.fr.php

@@ -42,6 +42,8 @@ return array (
 	'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',
+	'http_referer_is_ok'		=> 'Le HTTP REFERER est connu et semble correspondre à votre serveur.',
+	'http_referer_is_nok'		=> 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.',
 	'fix_errors_before'		=> 'Veuillez corriger les erreurs avant de passer à l’étape suivante.',
 
 	'general_conf_is_ok'		=> 'La configuration générale a été enregistrée.',

+ 37 - 15
app/install.php

@@ -149,7 +149,7 @@ function saveStep2() {
 
 		$config_array = array(
 			'language' => $_SESSION['language'],
-			'theme' => $_SESSION['theme'],
+			'theme' => 'Origine',
 			'old_entries' => $_SESSION['old_entries'],
 			'mail_login' => $_SESSION['mail_login'],
 			'passwordHash' => $_SESSION['passwordHash'],
@@ -307,6 +307,7 @@ function checkStep1() {
 	$log = LOG_PATH && is_writable(LOG_PATH);
 	$favicons = is_writable(DATA_PATH . '/favicons');
 	$persona = is_writable(DATA_PATH . '/persona');
+	$http_referer = is_referer_from_same_domain();
 
 	return array(
 		'php' => $php ? 'ok' : 'ko',
@@ -323,8 +324,10 @@ function checkStep1() {
 		'log' => $log ? 'ok' : 'ko',
 		'favicons' => $favicons ? 'ok' : 'ko',
 		'persona' => $persona ? 'ok' : 'ko',
+		'http_referer' => $http_referer ? 'ok' : 'ko',
 		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom &&
-		         $data && $cache && $log && $favicons && $persona ? 'ok' : 'ko'
+		         $data && $cache && $log && $favicons && $persona && $http_referer ?
+		         'ok' : 'ko'
 	);
 }
 
@@ -334,9 +337,15 @@ function checkStep2() {
 	        isset($_SESSION['mail_login']) &&
 	        !empty($_SESSION['default_user']);
 
-	$form = $_SESSION['auth_type'] != 'form' || !empty($_SESSION['passwordHash']);
+	$form = (
+		isset($_SESSION['auth_type']) &&
+		($_SESSION['auth_type'] != 'form' || !empty($_SESSION['passwordHash']))
+	);
 
-	$persona = $_SESSION['auth_type'] != 'persona' || !empty($_SESSION['mail_login']);
+	$persona = (
+		isset($_SESSION['auth_type']) &&
+		($_SESSION['auth_type'] != 'persona' || !empty($_SESSION['mail_login']))
+	);
 
 	$defaultUser = empty($_POST['default_user']) ? null : $_POST['default_user'];
 	if ($defaultUser === null) {
@@ -548,6 +557,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 . '/persona'); ?></p>
 	<?php } ?>
 
+	<?php if ($res['http_referer'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('ok'); ?></span> <?php echo _t('http_referer_is_ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('damn'); ?></span> <?php echo _t('http_referer_is_nok'); ?></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 { ?>
@@ -591,16 +606,17 @@ function printStep2() {
 		<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" onchange="auth_type_change()">
+				<select id="auth_type" name="auth_type" required="required" onchange="auth_type_change(true)">
 					<?php
-						function no_auth() {
-							return !in_array($_SESSION['auth_type'], array('form', 'persona', 'http_auth', 'none'));
+						function no_auth($auth_type) {
+							return !in_array($auth_type, array('form', 'persona', 'http_auth', 'none'));
 						}
+						$auth_type = isset($_SESSION['auth_type']) ? $_SESSION['auth_type'] : '';
 					?>
-					<option value="form"<?php echo $_SESSION['auth_type'] === 'form' || no_auth() ? ' selected="selected"' : '', cryptAvailable() ? '' : ' 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>
+					<option value="form"<?php echo $auth_type === 'form' || no_auth($auth_type) ? ' selected="selected"' : '', cryptAvailable() ? '' : ' disabled="disabled"'; ?>><?php echo _t('auth_form'); ?></option>
+					<option value="persona"<?php echo $auth_type === 'persona' ? ' selected="selected"' : ''; ?>><?php echo _t('auth_persona'); ?></option>
+					<option value="http_auth"<?php echo $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 $auth_type === 'none' ? ' selected="selected"' : ''; ?>><?php echo _t('auth_none'); ?></option>
 				</select>
 			</div>
 		</div>
@@ -609,7 +625,7 @@ function printStep2() {
 			<label class="group-name" for="passwordPlain"><?php echo _t('password_form'); ?></label>
 			<div class="group-controls">
 				<div class="stick">
-					<input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" autocomplete="off" <?php echo $_SESSION['auth_type'] === 'form' ? ' required="required"' : ''; ?> />
+					<input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" autocomplete="off" <?php echo $auth_type === 'form' ? ' required="required"' : ''; ?> />
 					<a class="btn toggle-password" data-toggle="passwordPlain"><?php echo FreshRSS_Themes::icon('key'); ?></a>
 				</div>
 				<noscript><b><?php echo _t('javascript_should_be_activated'); ?></b></noscript>
@@ -619,7 +635,7 @@ function printStep2() {
 		<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="alice@example.net" <?php echo $_SESSION['auth_type'] === 'persona' ? ' required="required"' : ''; ?> />
+				<input type="email" id="mail_login" name="mail_login" value="<?php echo isset($_SESSION['mail_login']) ? $_SESSION['mail_login'] : ''; ?>" placeholder="alice@example.net" <?php echo $auth_type === 'persona' ? ' required="required"' : ''; ?> />
 				<noscript><b><?php echo _t('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>
@@ -644,7 +660,7 @@ function printStep2() {
 				toggles[i].addEventListener('click', toggle_password);
 			}
 
-			function auth_type_change() {
+			function auth_type_change(focus) {
 				var auth_value = document.getElementById('auth_type').value,
 				    password_input = document.getElementById('passwordPlain'),
 				    mail_input = document.getElementById('mail_login');
@@ -652,15 +668,21 @@ function printStep2() {
 				if (auth_value === 'form') {
 					password_input.required = true;
 					mail_input.required = false;
+					if (focus) {
+						password_input.focus();
+					}
 				} else if (auth_value === 'persona') {
 					password_input.required = false;
 					mail_input.required = true;
+					if (focus) {
+						mail_input.focus();
+					}
 				} else {
 					password_input.required = false;
 					mail_input.required = false;
 				}
 			}
-			auth_type_change();
+			auth_type_change(false);
 		</script>
 
 		<div class="form-group form-actions">

+ 8 - 8
app/layout/aside_configure.phtml

@@ -1,32 +1,32 @@
 <ul class="nav nav-list aside">
 	<li class="nav-header"><?php echo _t('configuration'); ?></li>
-	<li class="item<?php echo Minz_Request::actionName() == 'display' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::actionName() === 'display' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('configure', 'display'); ?>"><?php echo _t('display_configuration'); ?></a>
 	</li>
-	<li class="item<?php echo Minz_Request::actionName() == 'reading' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::actionName() === 'reading' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('configure', 'reading'); ?>"><?php echo _t('reading_configuration'); ?></a>
 	</li>
-	<li class="item<?php echo Minz_Request::actionName() == 'archiving' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::actionName() === 'archiving' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('configure', 'archiving'); ?>"><?php echo _t('archiving_configuration'); ?></a>
 	</li>
-	<li class="item<?php echo Minz_Request::actionName() == 'sharing' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::actionName() === 'sharing' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('configure', 'sharing'); ?>"><?php echo _t('sharing'); ?></a>
 	</li>
-	<li class="item<?php echo Minz_Request::actionName() == 'shortcut' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::actionName() === 'shortcut' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('configure', 'shortcut'); ?>"><?php echo _t('shortcuts'); ?></a>
 	</li>
-	<li class="item<?php echo Minz_Request::actionName() == 'queries' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::actionName() === 'queries' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('configure', 'queries'); ?>"><?php echo _t('queries'); ?></a>
 	</li>
 	<li class="separator"></li>
-	<li class="item<?php echo Minz_Request::actionName() == 'users' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::actionName() === 'users' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('configure', 'users'); ?>"><?php echo _t('users'); ?></a>
 	</li>
 	<?php
 		$current_user = Minz_Session::param('currentUser', '');
 		if (Minz_Configuration::isAdmin($current_user)) {
 	?>
-	<li class="item<?php echo Minz_Request::controllerName() == 'update' ? ' active' : ''; ?>">
+	<li class="item<?php echo Minz_Request::controllerName() === 'update' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('update', 'index'); ?>"><?php echo _t('update'); ?></a>
 	</li>
 	<?php } ?>

+ 1 - 0
app/layout/layout.phtml

@@ -13,6 +13,7 @@
 	if (!empty($this->nextId)) {
 		$params = Minz_Request::params();
 		$params['next'] = $this->nextId;
+		$params['ajax'] = 1;
 ?>
 		<link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display(array('c' => Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), 'params' => $params)); ?>" />
 <?php } ?>

+ 14 - 8
app/views/configure/reading.phtml

@@ -10,6 +10,9 @@
 			<label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('articles_per_page'); ?></label>
 			<div class="group-controls">
 				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" min="5" max="50" />
+				<?php if ($this->conf->display_posts) { ?>
+				<?php echo _i('help'); ?> <?php echo _t('number_divided_when_unfolded'); ?>
+				<?php } ?>
 			</div>
 		</div>
 
@@ -31,14 +34,17 @@
 					<option value="reader"<?php echo $this->conf->view_mode === 'reader' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('reader_view'); ?></option>
 					<option value="global"<?php echo $this->conf->view_mode === 'global' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('global_view'); ?></option>
 				</select>
-				<label class="radio" for="radio_all">
-					<input type="radio" name="default_view" id="radio_all" value="<?php echo FreshRSS_Entry::STATE_ALL; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_ALL ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('show_all_articles'); ?>
-				</label>
-				<label class="radio" for="radio_not_read">
-					<input type="radio" name="default_view" id="radio_not_read" value="<?php echo FreshRSS_Entry::STATE_NOT_READ; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_NOT_READ ? ' checked="checked"' : ''; ?> />
-					<?php echo Minz_Translate::t ('show_not_reads'); ?>
-				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="view_mode"><?php echo _t('articles_to_display'); ?></label>
+			<div class="group-controls">
+				<select name="default_view" id="default_view">
+					<option value="<?php echo FreshRSS_Entry::STATE_NOT_READ; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_NOT_READ ? ' selected="selected"' : ''; ?>><?php echo _t('show_adaptive'); ?></option>
+					<option value="<?php echo FreshRSS_Entry::STATE_ALL; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_ALL ? ' selected="selected"' : ''; ?>><?php echo _t('show_all_articles'); ?></option>
+					<option value="<?php echo FreshRSS_Entry::STATE_NOT_READ_STRICT; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_NOT_READ_STRICT ? ' selected="selected"' : ''; ?>><?php echo _t('show_not_reads'); ?></option>
+				</select>
 			</div>
 		</div>
 

+ 4 - 1
app/views/helpers/pagination.phtml

@@ -9,7 +9,10 @@
 <ul class="pagination">
 	<li class="item pager-next">
 	<?php if (!empty($this->nextId)) { ?>
-		<?php $params['next'] = $this->nextId; ?>
+		<?php
+			$params['next'] = $this->nextId;
+			$params['ajax'] = 1;
+		?>
 		<a id="load_more" href="<?php echo Minz_Url::display(array('c' => $c, 'a' => $a, 'params' => $params)); ?>">
 			<?php echo _t('load_more'); ?>
 		</a>

+ 10 - 3
app/views/index/formLogin.phtml

@@ -3,7 +3,7 @@
 
 	switch (Minz_Configuration::authType()) {
 	case 'form':
-	?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
+	?><form id="crypto-form" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
 		<div>
 			<label for="username"><?php echo _t('username'); ?></label>
 			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
@@ -29,8 +29,15 @@
 
 	case 'persona':
 		?><p>
-			<?php echo _i('login'); ?>
-			<a class="signin" href="#"><?php echo _t('login_with_persona'); ?></a>
+			<a class="signin btn btn-important" href="#">
+				<?php echo _i('login'); ?>
+				<?php echo _t('login_with_persona'); ?>
+			</a><br /><br />
+
+			<?php echo _i('help'); ?>
+			<small>
+				<a href="<?php echo _url('index', 'resetAuth'); ?>"><?php echo _t('login_persona_problem'); ?></a>
+			</small>
 		</p><?php
 		break;
 	} ?>

+ 33 - 0
app/views/index/resetAuth.phtml

@@ -0,0 +1,33 @@
+<div class="prompt">
+	<h1><?php echo _t('auth_reset'); ?></h1>
+
+	<?php if (!empty($this->message)) { ?>
+	<p class="alert <?php echo $this->message['status'] === 'bad' ? 'alert-error' : 'alert-warn'; ?>">
+		<span class="alert-head"><?php echo $this->message['title']; ?></span><br />
+		<?php echo $this->message['body']; ?>
+	</p>
+	<?php } ?>
+
+	<?php if (!$this->no_form) { ?>
+	<form id="crypto-form" method="post" action="<?php echo _url('index', 'resetAuth'); ?>">
+		<p class="alert alert-warn">
+			<span class="alert-head"><?php echo _t('attention'); ?></span><br />
+			<?php echo _t('auth_will_reset'); ?>
+		</p>
+
+		<div>
+			<label for="username"><?php echo _t('username_admin'); ?></label>
+			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
+		</div>
+		<div>
+			<label for="passwordPlain"><?php echo _t('password'); ?></label>
+				<input type="password" id="passwordPlain" required="required" />
+				<input type="hidden" id="challenge" name="challenge" /><br />
+				<noscript><strong><?php echo _t('javascript_should_be_activated'); ?></strong></noscript>
+		</div>
+		<div>
+			<button id="loginButton" type="submit" class="btn btn-important"><?php echo _t('submit'); ?></button>
+		</div>
+	</form>
+	<?php } ?>
+</div>

+ 14 - 5
app/views/stats/idle.phtml

@@ -1,6 +1,6 @@
 <?php $this->partial('aside_stats'); ?>
 
-<div class="post content">
+<div class="post">
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
 
 	<h1><?php echo _t('stats_idle'); ?></h1>
@@ -12,11 +12,20 @@
 		<div class="stat">
 			<h2><?php echo _t($period); ?></h2>
 
-			<ul>
-				<?php foreach ($feeds as $feed) { ?>
-					<li><a href="<?php echo _url('configure', 'feed', 'id', $feed['id']); ?>" title="<?php echo date('Y-m-d', $feed['last_date']); ?>"><?php echo $feed['name']; ?></a></li>
-				<?php } ?>
+			<form id="form-delete" method="post" style="display: none"></form>
+
+			<?php foreach ($feeds as $feed) { ?>
+			<ul class="horizontal-list">
+				<li class="item">
+					<div class="stick">
+						<a class="btn" href="<?php echo _url('index', 'index', 'get', 'f_' . $feed['id']); ?>"><?php echo _i('link'); ?> <?php echo _t('filter'); ?></a>
+						<a class="btn" href="<?php echo _url('configure', 'feed', 'id', $feed['id']); ?>"><?php echo _i('configure'); ?> <?php echo _t('administration'); ?></a>
+						<button class="btn btn-attention confirm" form="form-delete" formaction="<?php echo _url('feed', 'delete', 'id', $feed['id']); ?>"><?php echo _t('delete'); ?></button>
+					</div>
+				</li>
+				<li class="item"><span title="<?php echo timestamptodate($feed['last_date'], false); ?>"><?php echo $feed['name']; ?></span></li>
 			</ul>
+			<?php } ?>
 		</div>
 	<?php
 			}

+ 23 - 23
app/views/stats/index.phtml

@@ -1,11 +1,11 @@
 <?php $this->partial('aside_stats'); ?>
 
-<div class="post content">
+<div class="post">
 	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo _t ('back_to_rss_feeds'); ?></a>
-	
+
 	<h1><?php echo _t ('stats_main'); ?></h1>
 
-	<div class="stat">
+	<div class="stat half">
 		<h2><?php echo _t ('stats_entry_repartition'); ?></h2>
 		<table>
 			<thead>
@@ -38,26 +38,9 @@
 				</tr>
 			</tbody>
 		</table>
-	</div>
-	
-	<div class="stat">
-		<h2><?php echo _t ('stats_entry_per_day'); ?></h2>
-		<div id="statsEntryPerDay" style="height: 300px"></div>
-	</div>
-	
-	<div class="stat">
-		<h2><?php echo _t ('stats_feed_per_category'); ?></h2>
-		<div id="statsFeedPerCategory" style="height: 300px"></div>
-		<div id="statsFeedPerCategoryLegend"></div>
-	</div>
-	
-	<div class="stat">
-		<h2><?php echo _t ('stats_entry_per_category'); ?></h2>
-		<div id="statsEntryPerCategory" style="height: 300px"></div>
-		<div id="statsEntryPerCategoryLegend"></div>
-	</div>
-	
-	<div class="stat">
+	</div><!--
+
+	--><div class="stat half">
 		<h2><?php echo _t ('stats_top_feed'); ?></h2>
 		<table>
 			<thead>
@@ -78,6 +61,23 @@
 			</tbody>
 		</table>
 	</div>
+
+	<div class="stat">
+		<h2><?php echo _t ('stats_entry_per_day'); ?></h2>
+		<div id="statsEntryPerDay" style="height: 300px"></div>
+	</div>
+
+	<div class="stat half">
+		<h2><?php echo _t ('stats_feed_per_category'); ?></h2>
+		<div id="statsFeedPerCategory" style="height: 300px"></div>
+		<div id="statsFeedPerCategoryLegend"></div>
+	</div><!--
+
+	--><div class="stat half">
+		<h2><?php echo _t ('stats_entry_per_category'); ?></h2>
+		<div id="statsEntryPerCategory" style="height: 300px"></div>
+		<div id="statsEntryPerCategoryLegend"></div>
+	</div>
 </div>
 
 <script>

+ 4 - 4
app/views/stats/repartition.phtml

@@ -1,6 +1,6 @@
 <?php $this->partial('aside_stats'); ?>
 
-<div class="post content">
+<div class="post ">
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('back_to_rss_feeds'); ?></a>
 
 	<h1><?php echo _t('stats_repartition'); ?></h1>
@@ -34,12 +34,12 @@
 		<div id="statsEntryPerHour" style="height: 300px"></div>
 	</div>
 
-	<div class="stat">
+	<div class="stat half">
 		<h2><?php echo _t('stats_entry_per_day_of_week'); ?></h2>
 		<div id="statsEntryPerDayOfWeek" style="height: 300px"></div>
-	</div>
+	</div><!--
 
-	<div class="stat">
+	--><div class="stat half">
 		<h2><?php echo _t('stats_entry_per_month'); ?></h2>
 		<div id="statsEntryPerMonth" style="height: 300px"></div>
 	</div>

+ 4 - 0
app/views/update/index.phtml

@@ -29,4 +29,8 @@
 		<a href="<?php echo _url('update', 'check'); ?>" class="btn"><?php echo _t('update_check'); ?></a>
 	</p>
 	<?php } ?>
+
+	<?php if ($this->update_to_apply) { ?>
+	<a class="btn btn-important" href="<?php echo _url('update', 'apply'); ?>"><?php echo _t('update_apply'); ?></a>
+	<?php } ?>
 </div>

+ 0 - 14
lib/Minz/Request.php

@@ -84,20 +84,6 @@ class Minz_Request {
 		return $_SERVER['HTTP_HOST'];
 	}
 
-	public static function isRefererFromSameDomain() {
-		if (empty($_SERVER['HTTP_REFERER'])) {
-			return false;
-		}
-		$host = parse_url(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://') .
-			(empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST']));
-		$referer = parse_url($_SERVER['HTTP_REFERER']);
-		if (empty($host['scheme']) || empty($referer['scheme']) || $host['scheme'] !== $referer['scheme'] ||
-		    empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) {
-			return false;
-		}
-		return (isset($host['port']) ? $host['port'] : 0) === (isset($referer['port']) ? $referer['port'] : 0);
-	}
-
 	/**
 	 * Détermine la base de l'url
 	 * @return la base de l'url

+ 14 - 0
lib/lib_rss.php

@@ -230,3 +230,17 @@ function cryptAvailable() {
 	}
 	return false;
 }
+
+function is_referer_from_same_domain() {
+	if (empty($_SERVER['HTTP_REFERER'])) {
+		return false;
+	}
+	$host = parse_url(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://') .
+		(empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST']));
+	$referer = parse_url($_SERVER['HTTP_REFERER']);
+	if (empty($host['scheme']) || empty($referer['scheme']) || $host['scheme'] !== $referer['scheme'] ||
+	    empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) {
+		return false;
+	}
+	return (isset($host['port']) ? $host['port'] : 0) === (isset($referer['port']) ? $referer['port'] : 0);
+}

+ 19 - 16
p/scripts/main.js

@@ -984,7 +984,7 @@ function init_load_more(box) {
 }
 //</endless_mode>
 
-//<Web login form>
+//<crypto form (Web login)>
 function poormanSalt() {	//If crypto.getRandomValues is not available
 	var text = '$2a$04$',
 		base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789/abcdefghijklmnopqrstuvwxyz';
@@ -994,20 +994,24 @@ function poormanSalt() {	//If crypto.getRandomValues is not available
 	return text;
 }
 
-function init_loginForm() {
-	var $loginForm = $('#loginForm');
-	if ($loginForm.length === 0) {
+function init_crypto_form() {
+	var $crypto_form = $('#crypto-form');
+	if ($crypto_form.length === 0) {
 		return;
 	}
+
 	if (!(window.dcodeIO)) {
 		if (window.console) {
 			console.log('FreshRSS waiting for bcrypt.js…');
 		}
-		window.setTimeout(init_loginForm, 100);
+		window.setTimeout(init_crypto_form, 100);
 		return;
 	}
-	$loginForm.on('submit', function() {
-		$('#loginButton').attr('disabled', '');
+
+	$crypto_form.on('submit', function() {
+		var $submit_button = $(this).find('button[type="submit"]');
+		$submit_button.attr('disabled', '');
+
 		var success = false;
 		$.ajax({
 			url: './?c=javascript&a=nonce&user=' + $('#username').val(),
@@ -1015,7 +1019,7 @@ function init_loginForm() {
 			async: false
 		}).done(function (data) {
 			if (data.salt1 == '' || data.nonce == '') {
-				alert('Invalid user!');
+				openNotification('Invalid user!', 'bad');
 			} else {
 				try {
 					var strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function'),
@@ -1023,22 +1027,23 @@ function init_loginForm() {
 						c = dcodeIO.bcrypt.hashSync(data.nonce + s, strong ? 4 : poormanSalt());
 					$('#challenge').val(c);
 					if (s == '' || c == '') {
-						alert('Crypto error!');
+						openNotification('Crypto error!', 'bad');
 					} else {
 						success = true;
 					}
 				} catch (e) {
-					alert('Crypto exception! ' + e);
+					openNotification('Crypto exception! ' + e, 'bad');
 				}
 			}
 		}).fail(function() {
-			alert('Communication error!');
+			openNotification('Communication error!', 'bad');
 		});
-		$('#loginButton').removeAttr('disabled');
+
+		$submit_button.removeAttr('disabled');
 		return success;
 	});
 }
-//</Web login form>
+//</crypto form (Web login)>
 
 //<persona>
 function init_persona() {
@@ -1240,14 +1245,12 @@ function init_all() {
 	}
 	init_notifications();
 	switch (authType) {
-		case 'form':
-			init_loginForm();
-			break;
 		case 'persona':
 			init_persona();
 			break;
 	}
 	init_confirm_action();
+	init_crypto_form();
 	$stream = $('#stream');
 	if ($stream.length > 0) {
 		init_actualize();

+ 12 - 1
p/themes/Dark/dark.css

@@ -872,7 +872,18 @@ a.btn {
 .stat > table td,
 .stat > table th {
 	border-bottom: 1px solid #333;
-	text-align: center;
+}
+
+.stat > .horizontal-list {
+	margin: 0 0 5px;
+}
+.stat > .horizontal-list .item {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.stat > .horizontal-list .item:first-child {
+	width: 270px;
 }
 
 /*=== LOGS */

+ 12 - 1
p/themes/Flat/flat.css

@@ -859,7 +859,18 @@ a.btn {
 .stat > table td,
 .stat > table th {
 	border-bottom: 1px solid #ddd;
-	text-align: center;
+}
+
+.stat > .horizontal-list {
+	margin: 0 0 5px;
+}
+.stat > .horizontal-list .item {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.stat > .horizontal-list .item:first-child {
+	width: 270px;
 }
 
 /*=== LOGS */

+ 14 - 3
p/themes/Origine/origine.css

@@ -808,12 +808,12 @@ a.btn {
 	background: #fafafa;
 }
 #bigMarkAsRead:hover {
-	color: #27ae60;
+	color: #0062be;
 	background: #fff;
 	box-shadow: 0 -5px 10px #eee inset;
 }
 #bigMarkAsRead:hover .bigTick {
-	text-shadow: 0 0 5px #27ae60;
+	text-shadow: 0 0 5px #0062be;
 }
 
 /*=== Navigation menu (for articles) */
@@ -913,7 +913,18 @@ a.btn {
 .stat > table td,
 .stat > table th {
 	border-bottom: 1px solid #ddd;
-	text-align: center;
+}
+
+.stat > .horizontal-list {
+	margin: 0 0 5px;
+}
+.stat > .horizontal-list .item {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.stat > .horizontal-list .item:first-child {
+	width: 270px;
 }
 
 /*=== LOGS */

+ 12 - 1
p/themes/Screwdriver/screwdriver.css

@@ -1025,7 +1025,18 @@ opacity: 1;
 	border-bottom: 1px solid #ccc;
 	background: rgba(255,255,255,0.38);
 	box-shadow: 0 1px #fff;
-	text-align: center;
+}
+
+.stat > .horizontal-list {
+	margin: 0 0 5px;
+}
+.stat > .horizontal-list .item {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.stat > .horizontal-list .item:first-child {
+	width: 250px;
 }
 
 /*=== LOGS */

+ 12 - 0
p/themes/base-theme/base.css

@@ -678,6 +678,18 @@ a.btn {
 	text-align: center;
 }
 
+.stat > .horizontal-list {
+	margin: 0 0 5px;
+}
+.stat > .horizontal-list .item {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+.stat > .horizontal-list .item:first-child {
+	width: 250px;
+}
+
 /*=== LOGS */
 /*=========*/
 .logs {

+ 23 - 0
p/themes/base-theme/template.css

@@ -98,6 +98,15 @@ button.as-link:active {
 	font-size: 1.1em;
 }
 
+/*=== Tables */
+table {
+	max-width: 100%;
+}
+th.numeric,
+td.numeric {
+	text-align: center;
+}
+
 /*=== COMPONENTS */
 /*===============*/
 /*=== Forms */
@@ -458,6 +467,12 @@ a.btn {
 .content pre {
 	overflow: auto;
 }
+br {
+	line-height: 1em;
+}
+br + br + br {
+	display: none;
+}
 
 /*=== Notification and actualize notification */
 .notification {
@@ -526,6 +541,14 @@ a.btn {
 }
 
 /*=== Statistiques */
+.stat {
+	margin: 15px 0;
+}
+.stat.half {
+	display: inline-block;
+	width: 46%;
+	padding: 0 2%;
+}
 .stat > table {
 	width: 100%;
 }