Просмотр исходного кода

Merge branch 'dev' into beta

Conflicts:
	README.fr.md
	README.md
Marien Fressinaud 11 лет назад
Родитель
Сommit
c29b6d4c22
59 измененных файлов с 1258 добавлено и 218 удалено
  1. 26 1
      CHANGELOG
  2. 39 0
      CREDITS
  3. 6 9
      README.fr.md
  4. 10 13
      README.md
  5. 21 19
      app/Controllers/errorController.php
  6. 1 1
      app/Controllers/importExportController.php
  7. 4 3
      app/Controllers/subscriptionController.php
  8. 4 1
      app/Controllers/updateController.php
  9. 5 7
      app/Models/ConfigurationSetter.php
  10. 4 7
      app/Models/Feed.php
  11. 10 5
      app/actualize_script.php
  12. 170 0
      app/i18n/de/admin.php
  13. 169 0
      app/i18n/de/conf.php
  14. 110 0
      app/i18n/de/feedback.php
  15. 163 0
      app/i18n/de/gen.php
  16. 61 0
      app/i18n/de/index.php
  17. 107 0
      app/i18n/de/install.php
  18. 61 0
      app/i18n/de/sub.php
  19. 1 0
      app/i18n/en/admin.php
  20. 1 0
      app/i18n/en/conf.php
  21. 6 2
      app/i18n/en/gen.php
  22. 1 2
      app/i18n/en/index.php
  23. 1 0
      app/i18n/en/install.php
  24. 1 0
      app/i18n/fr/admin.php
  25. 1 0
      app/i18n/fr/conf.php
  26. 6 2
      app/i18n/fr/gen.php
  27. 1 2
      app/i18n/fr/index.php
  28. 1 0
      app/i18n/fr/install.php
  29. 1 0
      app/install.php
  30. 5 5
      app/layout/aside_feed.phtml
  31. 1 2
      app/layout/header.phtml
  32. 1 2
      app/layout/nav_menu.phtml
  33. 1 1
      app/views/configure/shortcut.phtml
  34. 17 16
      app/views/helpers/javascript_vars.phtml
  35. 1 4
      app/views/index/about.phtml
  36. 1 1
      app/views/index/global.phtml
  37. 1 1
      app/views/subscription/index.phtml
  38. 1 0
      app/views/user/manage.phtml
  39. 1 0
      app/views/user/profile.phtml
  40. 6 9
      data/shares.php
  41. 0 7
      lib/Minz/Configuration.php
  42. 5 34
      lib/Minz/Error.php
  43. 4 1
      lib/Minz/ExtensionManager.php
  44. 3 1
      lib/Minz/Session.php
  45. 17 17
      lib/Minz/Url.php
  46. 2 2
      lib/SimplePie/SimplePie.php
  47. 1 1
      lib/SimplePie/SimplePie/File.php
  48. 102 26
      lib/lib_opml.php
  49. 12 2
      lib/lib_rss.php
  50. 14 7
      p/f.php
  51. 7 1
      p/scripts/main.js
  52. 1 1
      p/themes/BlueLagoon/README.md
  53. 6 0
      p/themes/Dark/dark.css
  54. 1 1
      p/themes/Screwdriver/README.md
  55. 1 1
      p/themes/base-theme/README.md
  56. 2 1
      p/themes/base-theme/template.css
  57. 32 0
      tests/app/Models/CategoryTest.php
  58. 7 0
      tests/bootstrap.php
  59. 13 0
      tests/phpunit.xml

+ 26 - 1
CHANGELOG

@@ -1,4 +1,29 @@
-# Journal des modifications
+# Changelog
+
+## 2015-01-31 FreshRSS 1.0.0 / 1.1.0 (beta)
+
+* UI
+	* Slider math with Dark theme
+	* Add a message if request failed for mark as read / favourite
+* I18n
+	* Fix some sentences
+	* Add German as a supported language
+	* Add some indications on password format
+* Bug fixing
+	* Some shortcuts was never saved
+	* Global view didn't work if set by default
+	* Minz_Error was badly raised
+	* Feed update failed if nothing had changed (MySQL only)
+	* CRON task failed with multiple users
+	* Tricky bug caused by cookie path
+	* Email sharing was badly supported (no urlencode())
+* Misc.
+	* Add a CREDIT file with contributor names
+	* Update lib_opml
+	* Default favicon is now served by HTTP code 200
+	* Change calls to syslog by Minz_Log::notice
+	* HTTP credentials are no longer logged
+
 
 ## 2015-01-15 FreshRSS 0.9.4 (beta)
 

+ 39 - 0
CREDITS

@@ -0,0 +1,39 @@
+This is a credit file of people who have contributed to FreshRSS with, at least,
+one commit on the FreshRSS repository (at https://github.com/FreshRSS/FreshRSS).
+Please note a commit on THIS specific file is not considered as a contribution
+(too easy!). It's purpose is to show even the smallest contribution is important.
+People are sorted by name so please keep this order.
+
+---
+
+Alexandre Alapetite
+https://github.com/Alkarex
+
+Alexis Degrugillier
+https://github.com/aledeg
+
+Alwaysin
+https://github.com/Alwaysin
+
+Amaury Carrade
+https://github.com/AmauryCarrade
+
+ealdraed
+https://github.com/ealdraed
+
+Luc Didry
+https://github.com/ldidry
+
+Marien Fressinaud
+dev@marienfressinaud.fr
+http://marienfressinaud.fr
+https://github.com/marienfressinaud
+
+Nicolas Elie
+https://github.com/nicolaselie
+
+plopoyop
+https://github.com/plopoyop
+
+tomgue
+https://github.com/tomgue

+ 6 - 9
README.fr.md

@@ -9,26 +9,23 @@ Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture an
 
 * Site officiel : http://freshrss.org
 * Démo : http://demo.freshrss.org/
-* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
-* Version actuelle : 0.9.4
-* Date de publication : 2015-01-15
-* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
+* Licence : [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
 
 ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
 
 # Note sur les branches
 **Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond :
 
-* Utilisez [la branche master](https://github.com/marienfressinaud/FreshRSS/tree/master/) si vous visez la stabilité.
-* [La branche beta](https://github.com/marienfressinaud/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois.
-* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/marienfressinaud/FreshRSS/tree/dev) vous ouvre les bras !
+* Utilisez [la branche master](https://github.com/FreshRSS/FreshRSS/tree/master/) si vous visez la stabilité.
+* [La branche beta](https://github.com/FreshRSS/FreshRSS/tree/beta) est celle par défaut : les nouveautés y sont ajoutées environ tous les mois.
+* Pour les développeurs et ceux qui savent ce qu'ils font, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras !
 
 # Disclaimer
 Cette application a été développée pour s’adapter à des besoins personnels et non professionnels.
 Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement.
 Je m’engage néanmoins à répondre dans la mesure du possible aux demandes d’évolution si celles-ci me semblent justifiées.
 Privilégiez pour cela des demandes sur GitHub
-(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail (dev@marienfressinaud.fr)
+(https://github.com/FreshRSS/FreshRSS/issues).
 
 # Pré-requis
 * Serveur modeste, par exemple sous Linux ou Windows
@@ -44,7 +41,7 @@ Privilégiez pour cela des demandes sur GitHub
 ![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
 
 # Installation
-1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
+1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip)
 2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
 3. Le serveur Web doit avoir les droits d’écriture dans le répertoire `./data/`
 4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation

+ 10 - 13
README.md

@@ -9,26 +9,23 @@ It is a multi-user application with an anonymous reading mode.
 
 * Official website: http://freshrss.org
 * Demo: http://demo.freshrss.org/
-* Developer: Marien Fressinaud <dev@marienfressinaud.fr>
-* Current version: 0.9.4
-* Publication date: 2015-01-15
-* License [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
+* License: [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html)
 
 ![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
 
 # Note on branches
 **This application is still in development!** Please use the branch that suits your needs:
 
-* Use [the master branch](https://github.com/marienfressinaud/FreshRSS/tree/master/) if you need a stable version.
-* [The beta branch](https://github.com/marienfressinaud/FreshRSS/tree/beta) is the default branch: new features are added on a monthly basis.
-* For developers and tech savvy persons, [the dev branch](https://github.com/marienfressinaud/FreshRSS/tree/dev) is waiting for you!
+* Use [the master branch](https://github.com/FreshRSS/FreshRSS/tree/master/) if you need a stable version.
+* [The beta branch](https://github.com/FreshRSS/FreshRSS/tree/beta) is the default branch: new features are added on a monthly basis.
+* For developers and tech savvy persons, [the dev branch](https://github.com/FreshRSS/FreshRSS/tree/dev) is waiting for you!
 
 # Disclaimer
 This application was developed to fulfill personal needs not professional needs.
 There is no guarantee neither on its security nor its proper functioning.
 If there is feature requests which I think are good for the project, I'll do my best to include them.
 The best way is to open issues on GitHub
-(https://github.com/marienfressinaud/FreshRSS/issues) or by email (dev@marienfressinaud.fr)
+(https://github.com/FreshRSS/FreshRSS/issues).
 
 # Requirements
 * Light server running Linux or Windows
@@ -37,14 +34,14 @@ The best way is to open issues on GitHub
 * PHP 5.2.1+ (PHP 5.3.7+ recommanded)
 	* Required extensions: [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (only for API access on platforms under 64 bits)
 	* Recommanded extensions : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
-* MySQL 5.0.3+ (recommanded) ou SQLite 3.7.4+
+* MySQL 5.0.3+ (recommanded) or SQLite 3.7.4+
 * A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
 	* Works on mobile
 
 ![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
 
 # Installation
-1. Get FreshRSS with git or [by downloading the archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
+1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip)
 2. Dump the application on your server (expose only the `./p/` folder)
 3. Add write access on `./data/` folder to the webserver user
 4. Access FreshRSS with your browser and follow the installation process
@@ -70,9 +67,9 @@ For example, if you want to run the script every hour:
 
 # Advices
 * For a better security, expose only the `./p/` folder on the web.
-	* Be aware that the `./data/` folder contain all personal data, so it is a bad idea to expose it.
-* The `./constants.php` file define access to application folder. If you want to customize your installation, every thing happens here.
-* If you encounter some problem, logs are accessibles from the interface or manually in `./data/log/*.log` files.
+	* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
+* The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here.
+* If you encounter any problem, logs are accessibles from the interface or manually in `./data/log/*.log` files.
 
 # Backup
 * You need to keep `./data/config.php`, `./data/*_user.php` and `./data/persona/` files

+ 21 - 19
app/Controllers/errorController.php

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

+ 1 - 1
app/Controllers/importExportController.php

@@ -151,7 +151,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	private function importOpml($opml_file) {
 		$opml_array = array();
 		try {
-			$opml_array = libopml_parse_string($opml_file);
+			$opml_array = libopml_parse_string($opml_file, false);
 		} catch (LibOPML_Exception $e) {
 			Minz_Log::warning($e->getMessage());
 			return true;

+ 4 - 3
app/Controllers/subscriptionController.php

@@ -102,13 +102,14 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 
 			invalidateHttpCache();
 
-			if ($feedDAO->updateFeed($id, $values)) {
+			$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id));
+			if ($feedDAO->updateFeed($id, $values) !== false) {
 				$this->view->feed->_category($cat);
 				$this->view->feed->faviconPrepare();
 
-				Minz_Request::good(_t('feedback.sub.feed.updated'), array('c' => 'subscription', 'params' => array('id' => $id)));
+				Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
 			} else {
-				Minz_Request::bad(_t('feedback.sub.error'), array('c' => 'subscription'));
+				Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
 			}
 		}
 	}

+ 4 - 1
app/Controllers/updateController.php

@@ -28,7 +28,10 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 			);
 		} elseif (file_exists(UPDATE_FILENAME)) {
 			// There is an update file to apply!
-			$version = file_get_contents(join_path(DATA_PATH, 'last_update.txt'));
+			$version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt'));
+			if (empty($version)) {
+				$version = 'unknown';
+			}
 			$this->view->update_to_apply = true;
 			$this->view->message = array(
 				'status' => 'good',

+ 5 - 7
app/Models/ConfigurationSetter.php

@@ -153,11 +153,11 @@ class FreshRSS_ConfigurationSetter {
 	}
 
 	private function _shortcuts(&$data, $values) {
-		foreach ($values as $key => $value) {
-			if (isset($data['shortcuts'][$key])) {
-				$data['shortcuts'][$key] = $value;
-			}
+		if (!is_array($values)) {
+			return;
 		}
+
+		$data['shortcuts'] = $values;
 	}
 
 	private function _sort_order(&$data, $value) {
@@ -210,9 +210,7 @@ class FreshRSS_ConfigurationSetter {
 
 	private function _mark_when(&$data, $values) {
 		foreach ($values as $key => $value) {
-			if (isset($data['mark_when'][$key])) {
-				$data['mark_when'][$key] = $this->handleBool($value);
-			}
+			$data['mark_when'][$key] = $this->handleBool($value);
 		}
 	}
 

+ 4 - 7
app/Models/Feed.php

@@ -240,19 +240,16 @@ class FreshRSS_Feed extends Minz_Model {
 					$subscribe_url = $feed->subscribe_url(true);
 				}
 
+				$clean_url = url_remove_credentials($subscribe_url);
 				if ($subscribe_url !== null && $subscribe_url !== $url) {
-					if ($this->httpAuth != '') {
-						// on enlève les id si authentification HTTP
-						$subscribe_url = preg_replace('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url);
-					}
-					$this->_url($subscribe_url);
+					$this->_url($clean_url);
 				}
 
 				if (($mtime === true) ||($mtime > $this->lastUpdate)) {
-					syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url);
+					Minz_Log::notice('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
 					$this->loadEntries($feed);	// et on charge les articles du flux
 				} else {
-					syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url);
+					Minz_Log::notice('FreshRSS use cache for ' . $clean_url);
 					$this->entries = array();
 				}
 

+ 10 - 5
app/actualize_script.php

@@ -21,8 +21,10 @@ $_GET['force'] = true;
 $_SERVER['HTTP_HOST'] = '';
 
 
+$log_file = join_path(USERS_PATH, '_', 'log.txt');
+
+
 $app = new FreshRSS();
-$app->init();
 
 $system_conf = Minz_Configuration::get('system');
 $system_conf->auth_type = 'none';  // avoid necessity to be logged in (not saved!)
@@ -42,13 +44,13 @@ $min_last_activity = time() - $limits['max_inactivity'];
 foreach ($users as $user) {
 	if (($user !== $system_conf->default_user) &&
 			(FreshRSS_UserDAO::mtime($user) < $min_last_activity)) {
-		syslog(LOG_INFO, 'FreshRSS skip inactive user ' . $user);
+		Minz_Log::notice('FreshRSS skip inactive user ' . $user, $log_file);
 		if (defined('STDOUT')) {
 			fwrite(STDOUT, 'FreshRSS skip inactive user ' . $user . "\n");	//Unbuffered
 		}
 		continue;
 	}
-	syslog(LOG_INFO, 'FreshRSS actualize ' . $user);
+	Minz_Log::notice('FreshRSS actualize ' . $user, $log_file);
 	if (defined('STDOUT')) {
 		fwrite(STDOUT, 'Actualize ' . $user . "...\n");	//Unbuffered
 	}
@@ -56,12 +58,15 @@ foreach ($users as $user) {
 
 
 	Minz_Session::_param('currentUser', $user);
+	new Minz_ModelPdo($user);	//TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init
 	FreshRSS_Auth::giveAccess();
+	$app->init();
 	$app->run();
 
 
 	if (!invalidateHttpCache()) {
-		syslog(LOG_NOTICE, 'FreshRSS write access problem in ' . join_path(USERS_PATH, $user, 'log.txt'));
+		Minz_Log::notice('FreshRSS write access problem in ' . join_path(USERS_PATH, $user, 'log.txt'),
+		                 $log_file);
 		if (defined('STDERR')) {
 			fwrite(STDERR, 'Write access problem in ' . join_path(USERS_PATH, $user, 'log.txt') . "\n");
 		}
@@ -69,7 +74,7 @@ foreach ($users as $user) {
 }
 
 
-syslog(LOG_INFO, 'FreshRSS actualize done.');
+Minz_Log::notice('FreshRSS actualize done.', $log_file);
 if (defined('STDOUT')) {
 	fwrite(STDOUT, 'Done.' . "\n");
 	$end_date = date_create('now');

+ 170 - 0
app/i18n/de/admin.php

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

+ 169 - 0
app/i18n/de/conf.php

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

+ 110 - 0
app/i18n/de/feedback.php

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

+ 163 - 0
app/i18n/de/gen.php

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

+ 61 - 0
app/i18n/de/index.php

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

+ 107 - 0
app/i18n/de/install.php

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

+ 61 - 0
app/i18n/de/sub.php

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

+ 1 - 0
app/i18n/en/admin.php

@@ -161,6 +161,7 @@ return array(
 		'email_persona' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Language',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
+		'password_format' => 'At least 7 characters',
 		'title' => 'Manage users',
 		'user_list' => 'List of users',
 		'username' => 'Username',

+ 1 - 0
app/i18n/en/conf.php

@@ -75,6 +75,7 @@ return array(
 		'email_persona' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'password_api' => 'Password API<br /><small>(e.g., for mobile apps)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
+		'password_format' => 'At least 7 characters',
 		'title' => 'Profile',
 	),
 	'reading' => array(

+ 6 - 2
app/i18n/en/gen.php

@@ -95,12 +95,16 @@ return array(
 		'category_empty' => 'Empty category',
 		'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
 		'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!',
+		'feedback' => array(
+			'body_new_articles' => 'There are \\d new articles to read on FreshRSS.',
+			'request_failed' => 'A request has failed, it may have been caused by Internet connection problems.',
+			'title_new_articles' => 'FreshRSS: new articles!',
+		),
 		'new_article' => 'There are new available articles, click to refresh the page.',
-		'notif_body_new_articles' => 'There are \\d new articles to read on FreshRSS.',
-		'notif_title_new_articles' => 'FreshRSS: new articles!',
 		'should_be_activated' => 'JavaScript must be enabled',
 	),
 	'lang' => array(
+		'de' => 'Deutsch',
 		'en' => 'English',
 		'fr' => 'Français',
 	),

+ 1 - 2
app/i18n/en/index.php

@@ -8,8 +8,7 @@ return array(
 		'credits' => 'Credits',
 		'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons are collected with <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
 		'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.',
-		'github_or_email' => '<a href="https://github.com/marienfressinaud/FreshRSS/issues">on Github</a> or <a href="mailto:dev@marienfressinaud.fr">by mail</a>',
-		'lead_developer' => 'Lead developer',
+		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>',
 		'license' => 'License',
 		'project_website' => 'Project website',
 		'title' => 'About',

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

@@ -12,6 +12,7 @@ return array(
 		'http' => 'HTTP (for advanced users with HTTPS)',
 		'none' => 'None (dangerous)',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
+		'password_format' => 'At least 7 characters',
 		'persona' => 'Mozilla Persona (modern, requires JavaScript)',
 		'type' => 'Authentication method',
 	),

+ 1 - 0
app/i18n/fr/admin.php

@@ -161,6 +161,7 @@ return array(
 		'email_persona' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Langue',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+		'password_format' => '7 caractères minimum',
 		'title' => 'Gestion des utilisateurs',
 		'user_list' => 'Liste des utilisateurs',
 		'username' => 'Nom d’utilisateur',

+ 1 - 0
app/i18n/fr/conf.php

@@ -75,6 +75,7 @@ return array(
 		'email_persona' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+		'password_format' => '7 caractères minimum',
 		'title' => 'Profil',
 	),
 	'reading' => array(

+ 6 - 2
app/i18n/fr/gen.php

@@ -95,12 +95,16 @@ return array(
 		'category_empty' => 'Catégorie vide',
 		'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
 		'confirm_action_feed_cat' => 'Êtes-vous sûr(e) de vouloir continuer ? Vous perdrez les favoris et les filtres associés. Cette action ne peut être annulée !',
+		'feedback' => array(
+			'body_new_articles' => 'Il y a \\d nouveaux articles à lire sur FreshRSS.',
+			'request_failed' => 'Une requête a échoué, cela peut être dû à des problèmes de connexion à Internet.',
+			'title_new_articles' => 'FreshRSS : nouveaux articles !',
+		),
 		'new_article' => 'Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.',
-		'notif_body_new_articles' => 'Il y a \\d nouveaux articles à lire sur FreshRSS.',
-		'notif_title_new_articles' => 'FreshRSS : nouveaux articles !',
 		'should_be_activated' => 'Le JavaScript doit être activé.',
 	),
 	'lang' => array(
+		'de' => 'Deutsch',
 		'en' => 'English',
 		'fr' => 'Français',
 	),

+ 1 - 2
app/i18n/fr/index.php

@@ -8,8 +8,7 @@ return array(
 		'credits' => 'Crédits',
 		'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n’utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Les favicons sont récupérés grâce au site <a href="https://getfavicon.appspot.com/">getFavicon</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
 		'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.',
-		'github_or_email' => '<a href="https://github.com/marienfressinaud/FreshRSS/issues">sur Github</a> ou <a href="mailto:dev@marienfressinaud.fr">par courriel</a>',
-		'lead_developer' => 'Développeur principal',
+		'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">sur Github</a>',
 		'license' => 'Licence',
 		'project_website' => 'Site du projet',
 		'title' => 'À propos',

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

@@ -12,6 +12,7 @@ return array(
 		'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
 		'none' => 'Aucune (dangereux)',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+		'password_format' => '7 caractères minimum',
 		'persona' => 'Mozilla Persona (moderne, requiert JavaScript)',
 		'type' => 'Méthode d’authentification',
 	),

+ 1 - 0
app/install.php

@@ -598,6 +598,7 @@ function printStep2() {
 					<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>
+				<?php echo _i('help'); ?> <?php echo _t('install.auth.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 			</div>
 		</div>

+ 5 - 5
app/layout/aside_feed.phtml

@@ -74,20 +74,20 @@
 <script id="feed_config_template" type="text/html">
 	<ul class="dropdown-menu">
 		<li class="dropdown-close"><a href="#close">❌</a></li>
-		<li class="item"><a href="<?php echo _url('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo _t('gen.action.filter'); ?></a></li>
+		<li class="item"><a href="<?php echo _url('index', 'index', 'get', 'f_------'); ?>"><?php echo _t('gen.action.filter'); ?></a></li>
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
-		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '!!!!!!'); ?>"><?php echo _t('index.menu.stats'); ?></a></li>
+		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '------'); ?>"><?php echo _t('index.menu.stats'); ?></a></li>
 		<?php } ?>
 		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li>
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<li class="separator"></li>
-		<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '!!!!!!'); ?>"><?php echo _t('gen.action.manage'); ?></a></li>
-		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo _t('gen.action.actualize'); ?></a></li>
+		<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '------'); ?>"><?php echo _t('gen.action.manage'); ?></a></li>
+		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '------'); ?>"><?php echo _t('gen.action.actualize'); ?></a></li>
 		<li class="item">
 			<?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>
 			<button class="read_all as-link <?php echo $confirm; ?>"
 			        form="mark-read-aside"
-			        formaction="<?php echo _url('entry', 'read', 'get', 'f_!!!!!!'); ?>"
+			        formaction="<?php echo _url('entry', 'read', 'get', 'f_------'); ?>"
 			        type="submit"><?php echo _t('gen.action.mark_read'); ?></button>
 		</li>
 		<?php } ?>

+ 1 - 2
app/layout/header.phtml

@@ -25,8 +25,7 @@ if (FreshRSS_Auth::accessNeedsAction()) {
 		<?php if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::$system_conf->allow_anonymous) { ?>
 		<form action="<?php echo _url('index', 'index'); ?>" method="get">
 			<div class="stick">
-				<?php $search = Minz_Request::param('search', ''); ?>
-				<input type="search" name="search" id="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo _t('gen.menu.search'); ?>" />
+				<input type="search" name="search" id="search" class="extend" value="<?php echo FreshRSS_Context::$search; ?>" placeholder="<?php echo _t('gen.menu.search'); ?>" />
 
 				<?php $get = Minz_Request::param('get', ''); ?>
 				<?php if ($get != '') { ?>

+ 1 - 2
app/layout/nav_menu.phtml

@@ -156,8 +156,7 @@
 
 	<div class="item search">
 		<form action="<?php echo _url('index', 'index'); ?>" method="get">
-			<?php $search = Minz_Request::param('search', ''); ?>
-			<input type="search" name="search" class="extend" value="<?php echo $search; ?>" placeholder="<?php echo _t('index.menu.search_short'); ?>" />
+			<input type="search" name="search" class="extend" value="<?php echo FreshRSS_Context::$search; ?>" placeholder="<?php echo _t('index.menu.search_short'); ?>" />
 
 			<?php $get = Minz_Request::param('get', ''); ?>
 			<?php if($get != '') { ?>

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

@@ -114,7 +114,7 @@
 		<div class="form-group">
 			<label class="group-name" for="close_dropdown_shortcut"><?php echo _t('conf.shortcut.close_dropdown'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="help_shortcut" name="shortcuts[close_dropdown]" list="keys" value="<?php echo $s['close_dropdown']; ?>" />
+				<input type="text" id="close_dropdown" name="shortcuts[close_dropdown]" list="keys" value="<?php echo $s['close_dropdown']; ?>" />
 			</div>
 		</div>
 

+ 17 - 16
app/views/helpers/javascript_vars.phtml

@@ -35,20 +35,20 @@ echo 'var context={',
 "},\n";
 
 echo 'shortcuts={',
-	'mark_read:"', $s['mark_read'], '",',
-	'mark_favorite:"', $s['mark_favorite'], '",',
-	'go_website:"', $s['go_website'], '",',
-	'prev_entry:"', $s['prev_entry'], '",',
-	'next_entry:"', $s['next_entry'], '",',
-	'first_entry:"', $s['first_entry'], '",',
-	'last_entry:"', $s['last_entry'], '",',
-	'collapse_entry:"', $s['collapse_entry'], '",',
-	'load_more:"', $s['load_more'], '",',
-	'auto_share:"', $s['auto_share'], '",',
-	'focus_search:"', $s['focus_search'], '",',
-	'user_filter:"', $s['user_filter'], '",',
-	'help:"', $s['help'], '",',
-	'close_dropdown:"', $s['close_dropdown'], '"',
+	'mark_read:"', @$s['mark_read'], '",',
+	'mark_favorite:"', @$s['mark_favorite'], '",',
+	'go_website:"', @$s['go_website'], '",',
+	'prev_entry:"', @$s['prev_entry'], '",',
+	'next_entry:"', @$s['next_entry'], '",',
+	'first_entry:"', @$s['first_entry'], '",',
+	'last_entry:"', @$s['last_entry'], '",',
+	'collapse_entry:"', @$s['collapse_entry'], '",',
+	'load_more:"', @$s['load_more'], '",',
+	'auto_share:"', @$s['auto_share'], '",',
+	'focus_search:"', @$s['focus_search'], '",',
+	'user_filter:"', @$s['user_filter'], '",',
+	'help:"', @$s['help'], '",',
+	'close_dropdown:"', @$s['close_dropdown'], '"',
 "},\n";
 
 echo 'url={',
@@ -60,8 +60,9 @@ echo 'url={',
 
 echo 'i18n={',
 	'confirmation_default:"', _t('gen.js.confirm_action'), '",',
-	'notif_title_articles:"', _t('gen.js.notif_title_new_articles'), '",',
-	'notif_body_articles:"', _t('gen.js.notif_body_new_articles'), '",',
+	'notif_title_articles:"', _t('gen.js.feedback.title_new_articles'), '",',
+	'notif_body_articles:"', _t('gen.js.feedback.body_new_articles'), '",',
+	'notif_request_failed:"', _t('gen.js.feedback.request_failed'), '",',
 	'category_empty:"', _t('gen.js.category_empty'), '"',
 "},\n";
 

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

@@ -7,11 +7,8 @@
 		<dt><?php echo _t('index.about.project_website'); ?></dt>
 		<dd><a href="<?php echo FRESHRSS_WEBSITE; ?>"><?php echo FRESHRSS_WEBSITE; ?></a></dd>
 
-		<dt><?php echo _t('index.about.lead_developer'); ?></dt>
-		<dd><a href="mailto:contact@marienfressinaud.fr">Marien Fressinaud</a> — <a href="http://marienfressinaud.fr"><?php echo _t('index.about.website'); ?></a></dd>
-
 		<dt><?php echo _t('index.about.bugs_reports'); ?></dt>
-		<dd><?php echo _t('index.about.github_or_email'); ?></dd>
+		<dd><?php echo _t('index.about.github'); ?></dd>
 
 		<dt><?php echo _t('index.about.license'); ?></dt>
 		<dd><?php echo _t('index.about.agpl3'); ?></dd>

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

@@ -13,7 +13,7 @@
 <?php
 	$url_base = array(
 		'c' => 'index',
-		'a' => 'index',
+		'a' => 'normal',
 		'params' => Minz_Request::params()
 	);
 

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

@@ -7,7 +7,7 @@
 
 	<form id="add_rss" method="post" action="<?php echo _url('feed', 'add'); ?>" autocomplete="off">
 		<div class="stick">
-			<input type="url" name="url_rss" class="extend" placeholder="<?php echo _t('sub.feed.add'); ?>" />
+			<input type="url" name="url_rss" class="long" placeholder="<?php echo _t('sub.feed.add'); ?>" />
 			<div class="dropdown">
 				<div id="dropdown-cat" class="dropdown-target"></div>
 

+ 1 - 0
app/views/user/manage.phtml

@@ -32,6 +32,7 @@
 					<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
 					<a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
 				</div>
+				<?php echo _i('help'); ?> <?php echo _t('admin.user.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 			</div>
 		</div>

+ 1 - 0
app/views/user/profile.phtml

@@ -24,6 +24,7 @@
 					<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
 					<a class="btn toggle-password" data-toggle="passwordPlain"><?php echo _i('key'); ?></a>
 				</div>
+				<?php echo _i('help'); ?> <?php echo _t('conf.profile.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 			</div>
 		</div>

+ 6 - 9
data/shares.php

@@ -19,7 +19,7 @@
 return array(
 	'shaarli' => array(
 		'url' => '~URL~?post=~LINK~&amp;title=~TITLE~&amp;source=FreshRSS',
-		'transform' => array('urlencode'),
+		'transform' => array('rawurlencode'),
 		'help' => 'http://sebsauvage.net/wiki/doku.php?id=php:shaarli',
 		'form' => 'advanced',
 	),
@@ -40,31 +40,28 @@ return array(
 	),
 	'diaspora' => array(
 		'url' => '~URL~/bookmarklet?url=~LINK~&amp;title=~TITLE~',
-		'transform' => array('urlencode'),
+		'transform' => array('rawurlencode'),
 		'help' => 'https://diasporafoundation.org/',
 		'form' => 'advanced',
 	),
 	'twitter' => array(
 		'url' => 'https://twitter.com/share?url=~LINK~&amp;text=~TITLE~',
-		'transform' => array('urlencode'),
+		'transform' => array('rawurlencode'),
 		'form' => 'simple',
 	),
 	'g+' => array(
 		'url' => 'https://plus.google.com/share?url=~LINK~',
-		'transform' => array('urlencode'),
+		'transform' => array('rawurlencode'),
 		'form' => 'simple',
 	),
 	'facebook' => array(
 		'url' => 'https://www.facebook.com/sharer.php?u=~LINK~&amp;t=~TITLE~',
-		'transform' => array('urlencode'),
+		'transform' => array('rawurlencode'),
 		'form' => 'simple',
 	),
 	'email' => array(
 		'url' => 'mailto:?subject=~TITLE~&amp;body=~LINK~',
-		'transform' => array(
-			'link' => array('urlencode'),
-			'title' => array(),
-		),
+		'transform' => array('rawurlencode'),
 		'form' => 'simple',
 	),
 	'print' => array(

+ 0 - 7
lib/Minz/Configuration.php

@@ -16,16 +16,9 @@ class Minz_Configuration {
 	 * @param $config_filename the filename of the configuration
 	 * @param $default_filename a filename containing default values for the configuration
 	 * @param $configuration_setter an optional helper to set values in configuration
-	 * @throws Minz_ConfigurationNamespaceException if the namespace already exists.
 	 */
 	public static function register($namespace, $config_filename, $default_filename = null,
 	                                $configuration_setter = null) {
-		if (isset(self::$config_list[$namespace])) {
-			throw new Minz_ConfigurationNamespaceException(
-				$namespace . ' namespace already exists'
-			);
-		}
-
 		self::$config_list[$namespace] = new Minz_Configuration(
 			$namespace, $config_filename, $default_filename, $configuration_setter
 		);

+ 5 - 34
lib/Minz/Error.php

@@ -23,42 +23,13 @@ class Minz_Error {
 		$logs = self::processLogs ($logs);
 		$error_filename = APP_PATH . '/Controllers/errorController.php';
 
-		switch ($code) {
-			case 200 :
-				header('HTTP/1.1 200 OK');
-				break;
-			case 403 :
-				header('HTTP/1.1 403 Forbidden');
-				break;
-			case 404 :
-				header('HTTP/1.1 404 Not Found');
-				break;
-			case 500 :
-				header('HTTP/1.1 500 Internal Server Error');
-				break;
-			case 503 :
-				header('HTTP/1.1 503 Service Unavailable');
-				break;
-			default :
-				header('HTTP/1.1 500 Internal Server Error');
-		}
-
 		if (file_exists ($error_filename)) {
-			$params = array (
-				'code' => $code,
-				'logs' => $logs
-			);
+			Minz_Session::_param('error_code', $code);
+			Minz_Session::_param('error_logs', $logs);
 
-			if ($redirect) {
-				Minz_Request::forward (array (
-					'c' => 'error'
-				), true);
-			} else {
-				Minz_Request::forward (array (
-					'c' => 'error',
-					'params' => $params
-				), false);
-			}
+			Minz_Request::forward (array (
+				'c' => 'error'
+			), $redirect);
 		} else {
 			echo '<h1>An error occured</h1>' . "\n";
 

+ 4 - 1
lib/Minz/ExtensionManager.php

@@ -56,6 +56,9 @@ class Minz_ExtensionManager {
 
 		foreach ($list_potential_extensions as $ext_dir) {
 			$ext_pathname = EXTENSIONS_PATH . '/' . $ext_dir;
+			if (!is_dir($ext_pathname)) {
+				continue;
+			}
 			$metadata_filename = $ext_pathname . '/' . self::$ext_metaname;
 
 			// Try to load metadata file.
@@ -111,7 +114,7 @@ class Minz_ExtensionManager {
 		$entry_point_filename = $info['path'] . '/' . self::$ext_entry_point;
 		$ext_class_name = $info['entrypoint'] . 'Extension';
 
-		include($entry_point_filename);
+		include_once($entry_point_filename);
 
 		// Test if the given extension class exists.
 		if (!class_exists($ext_class_name)) {

+ 3 - 1
lib/Minz/Session.php

@@ -65,7 +65,9 @@ class Minz_Session {
 	 * @param $l la durée de vie
 	 */
 	public static function keepCookie($l) {
-		$cookie_dir = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
+		// Get the script_name (e.g. /p/i/index.php) and keep only the path.
+		$cookie_dir = empty($_SERVER['SCRIPT_NAME']) ? '' : $_SERVER['SCRIPT_NAME'];
+		$cookie_dir = dirname($cookie_dir);
 		session_set_cookie_params($l, $cookie_dir, '', false, true);
 	}
 

+ 17 - 17
lib/Minz/Url.php

@@ -45,45 +45,45 @@ class Minz_Url {
 
 		return $url_string;
 	}
-	
+
 	/**
 	 * Construit l'URI d'une URL
 	 * @param l'url sous forme de tableau
 	 * @param $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
 	 * @return l'uri sous la forme ?key=value&key2=value2
 	 */
-	private static function printUri ($url, $encodage) {
+	private static function printUri($url, $encodage) {
 		$uri = '';
 		$separator = '?';
-		
-		if($encodage == 'html') {
+
+		if ($encodage === 'html') {
 			$and = '&amp;';
 		} else {
 			$and = '&';
 		}
-		
-		if (isset ($url['c'])
-		 && $url['c'] != Minz_Request::defaultControllerName ()) {
+
+		if (isset($url['c'])
+		 && $url['c'] != Minz_Request::defaultControllerName()) {
 			$uri .= $separator . 'c=' . $url['c'];
 			$separator = $and;
 		}
-		
-		if (isset ($url['a'])
-		 && $url['a'] != Minz_Request::defaultActionName ()) {
+
+		if (isset($url['a'])
+		 && $url['a'] != Minz_Request::defaultActionName()) {
 			$uri .= $separator . 'a=' . $url['a'];
 			$separator = $and;
 		}
-		
-		if (isset ($url['params'])) {
+
+		if (isset($url['params'])) {
 			foreach ($url['params'] as $key => $param) {
-				$uri .= $separator . $key . '=' . $param;
+				$uri .= $separator . urlencode($key) . '=' . urlencode($param);
 				$separator = $and;
 			}
 		}
-		
+
 		return $uri;
 	}
-	
+
 	/**
 	 * Vérifie que les éléments du tableau représentant une url soit ok
 	 * @param l'url sous forme de tableau (sinon renverra directement $url)
@@ -91,7 +91,7 @@ class Minz_Url {
 	 */
 	public static function checkUrl ($url) {
 		$url_checked = $url;
-		
+
 		if (is_array ($url)) {
 			if (!isset ($url['c'])) {
 				$url_checked['c'] = Minz_Request::defaultControllerName ();
@@ -103,7 +103,7 @@ class Minz_Url {
 				$url_checked['params'] = array ();
 			}
 		}
-		
+
 		return $url_checked;
 	}
 }

+ 2 - 2
lib/SimplePie/SimplePie.php

@@ -1529,11 +1529,11 @@ class SimplePie
 					{	//FreshRSS
 						$md5 = $this->cleanMd5($file->body);
 						if ($this->data['md5'] === $md5) {
-							syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url);
+							// syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url);
 							$cache->touch();
 							return true;	//Content unchanged even though server did not send a 304
 						} else {
-							syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url);
+							// syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url);
 							$this->data['md5'] = $md5;
 						}
 					}

+ 1 - 1
lib/SimplePie/SimplePie/File.php

@@ -79,7 +79,7 @@ class SimplePie_File
 		$this->useragent = $useragent;
 		if (preg_match('/^http(s)?:\/\//i', $url))
 		{
-			syslog(LOG_INFO, 'SimplePie GET ' . $url);	//FreshRSS
+			// syslog(LOG_INFO, 'SimplePie GET ' . $url);	//FreshRSS
 			if ($useragent === null)
 			{
 				$useragent = ini_get('user_agent');

+ 102 - 26
lib/lib_opml.php

@@ -1,11 +1,19 @@
 <?php
 
-/* *
+/**
  * lib_opml is a free library to manage OPML format in PHP.
- * It takes in consideration only version 2.0 (http://dev.opml.org/spec2.html).
- * Basically it means "text" attribute for outline elements is required.
  *
- * lib_opml requires SimpleXML (http://php.net/manual/en/book.simplexml.php)
+ * By default, it takes in consideration version 2.0 but can be compatible with
+ * OPML 1.0. More information on http://dev.opml.org.
+ * Difference is "text" attribute is optional in version 1.0. It is highly
+ * recommended to use this attribute.
+ *
+ * lib_opml requires SimpleXML (php.net/simplexml) and DOMDocument (php.net/domdocument)
+ *
+ * @author   Marien Fressinaud <dev@marienfressinaud.fr>
+ * @link     https://github.com/marienfressinaud/lib_opml
+ * @version  0.2
+ * @license  public domain
  *
  * Usages:
  * > include('lib_opml.php');
@@ -23,21 +31,44 @@
  * > echo $opml_string;
  * > print_r($opml_object);
  *
+ * You can set $strict argument to false if you want to bypass "text" attribute
+ * requirement.
+ *
  * If parsing fails for any reason (e.g. not an XML string, does not match with
  * the specifications), a LibOPML_Exception is raised.
  *
- * Author: Marien Fressinaud <dev@marienfressinaud.fr>
- * Url: https://github.com/marienfressinaud/lib_opml
- * Version: 0.1
- * Date: 2014-03-29
- * License: public domain
+ * lib_opml array format is described here:
+ * $array = array(
+ *     'head' => array(       // 'head' element is optional (but recommended)
+ *         'key' => 'value',  // key must be a part of available OPML head elements
+ *     ),
+ *     'body' => array(              // body is required
+ *         array(                    // this array represents an outline (at least one)
+ *             'text' => 'value',    // 'text' element is required if $strict is true
+ *             'key' => 'value',     // key and value are what you want (optional)
+ *             '@outlines' = array(  // @outlines is a special value and represents sub-outlines
+ *                 array(
+ *                     [...]         // where [...] is a valid outline definition
+ *                 ),
+ *             ),
+ *         ),
+ *         array(                    // other outline definitions
+ *             [...]
+ *         ),
+ *         [...],
+ *     )
+ * )
  *
- * */
+ */
 
+/**
+ * A simple Exception class which represents any kind of OPML problem.
+ * Message should precise the current problem.
+ */
 class LibOPML_Exception extends Exception {}
 
 
-// These elements are optional
+// Define the list of available head attributes. All of them are optional.
 define('HEAD_ELEMENTS', serialize(array(
 	'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
 	'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop',
@@ -45,7 +76,16 @@ define('HEAD_ELEMENTS', serialize(array(
 )));
 
 
-function libopml_parse_outline($outline_xml) {
+/**
+ * Parse an XML object as an outline object and return corresponding array
+ *
+ * @param SimpleXMLElement $outline_xml the XML object we want to parse
+ * @param bool $strict true if "text" attribute is required, false else
+ * @return array corresponding to an outline and following format described above
+ * @throws LibOPML_Exception
+ * @access private
+ */
+function libopml_parse_outline($outline_xml, $strict = true) {
 	$outline = array();
 
 	// An outline may contain any kind of attributes but "text" attribute is
@@ -59,7 +99,7 @@ function libopml_parse_outline($outline_xml) {
 		}
 	}
 
-	if (!$text_is_present) {
+	if (!$text_is_present && $strict) {
 		throw new LibOPML_Exception(
 			'Outline does not contain any text attribute'
 		);
@@ -68,7 +108,7 @@ function libopml_parse_outline($outline_xml) {
 	foreach ($outline_xml->children() as $key => $value) {
 		// An outline may contain any number of outline children
 		if ($key === 'outline') {
-			$outline['@outlines'][] = libopml_parse_outline($value);
+			$outline['@outlines'][] = libopml_parse_outline($value, $strict);
 		} else {
 			throw new LibOPML_Exception(
 				'Body can contain only outline elements'
@@ -80,7 +120,16 @@ function libopml_parse_outline($outline_xml) {
 }
 
 
-function libopml_parse_string($xml) {
+/**
+ * Parse a string as a XML one and returns the corresponding array
+ *
+ * @param string $xml is the string we want to parse
+ * @param bool $strict true if "text" attribute is required, false else
+ * @return array corresponding to the XML string and following format described above
+ * @throws LibOPML_Exception
+ * @access public
+ */
+function libopml_parse_string($xml, $strict = true) {
 	$dom = new DOMDocument();
 	$dom->recover = true;
 	$dom->strictErrorChecking = false;
@@ -101,7 +150,6 @@ function libopml_parse_string($xml) {
 
 	// First, we get all "head" elements. Head is required but its sub-elements
 	// are optional.
-	// TODO: test head exists!
 	foreach ($opml->head->children() as $key => $value) {
 		if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
 			$array['head'][$key] = (string)$value;
@@ -115,11 +163,10 @@ function libopml_parse_string($xml) {
 	// Then, we get body oulines. Body must contain at least one outline
 	// element.
 	$at_least_one_outline = false;
-	// TODO: test body exists!
 	foreach ($opml->body->children() as $key => $value) {
 		if ($key === 'outline') {
 			$at_least_one_outline = true;
-			$array['body'][] = libopml_parse_outline($value);
+			$array['body'][] = libopml_parse_outline($value, $strict);
 		} else {
 			throw new LibOPML_Exception(
 				'Body can contain only outline elements'
@@ -137,7 +184,16 @@ function libopml_parse_string($xml) {
 }
 
 
-function libopml_parse_file($filename) {
+/**
+ * Parse a string contained into a file as a XML string and returns the corresponding array
+ *
+ * @param string $filename should indicates a valid XML file
+ * @param bool $strict true if "text" attribute is required, false else
+ * @return array corresponding to the file content and following format described above
+ * @throws LibOPML_Exception
+ * @access public
+ */
+function libopml_parse_file($filename, $strict = true) {
 	$file_content = file_get_contents($filename);
 
 	if ($file_content === false) {
@@ -146,11 +202,20 @@ function libopml_parse_file($filename) {
 		);
 	}
 
-	return libopml_parse_string($file_content);
+	return libopml_parse_string($file_content, $strict);
 }
 
 
-function libopml_render_outline($parent_elt, $outline) {
+/**
+ * Create a XML outline object in a parent object.
+ *
+ * @param SimpleXMLElement $parent_elt is the parent object of current outline
+ * @param array $outline array representing an outline object
+ * @param bool $strict true if "text" attribute is required, false else
+ * @throws LibOPML_Exception
+ * @access private
+ */
+function libopml_render_outline($parent_elt, $outline, $strict) {
 	// Outline MUST be an array!
 	if (!is_array($outline)) {
 		throw new LibOPML_Exception(
@@ -165,7 +230,7 @@ function libopml_render_outline($parent_elt, $outline) {
 		// outline elements.
 		if ($key === '@outlines' && is_array($value)) {
 			foreach ($value as $outline_child) {
-				libopml_render_outline($outline_elt, $outline_child);
+				libopml_render_outline($outline_elt, $outline_child, $strict);
 			}
 		} elseif (is_array($value)) {
 			throw new LibOPML_Exception(
@@ -181,7 +246,7 @@ function libopml_render_outline($parent_elt, $outline) {
 		}
 	}
 
-	if (!$text_is_present) {
+	if (!$text_is_present && $strict) {
 		throw new LibOPML_Exception(
 			'You must define at least a text element for all outlines'
 		);
@@ -189,8 +254,19 @@ function libopml_render_outline($parent_elt, $outline) {
 }
 
 
-function libopml_render($array, $as_xml_object = false) {
-	$opml = new SimpleXMLElement('<opml version="2.0"></opml>');
+/**
+ * Render an array as an OPML string or a XML object.
+ *
+ * @param array $array is the array we want to render and must follow structure defined above
+ * @param bool $as_xml_object false if function must return a string, true for a XML object
+ * @param bool $strict true if "text" attribute is required, false else
+ * @return string|SimpleXMLElement XML string corresponding to $array or XML object
+ * @throws LibOPML_Exception
+ * @access public
+ */
+function libopml_render($array, $as_xml_object = false, $strict = true) {
+	$opml = new SimpleXMLElement('<opml></opml>');
+	$opml->addAttribute('version', $strict ? '2.0' : '1.0');
 
 	// Create head element. $array['head'] is optional but head element will
 	// exist in the final XML object.
@@ -218,7 +294,7 @@ function libopml_render($array, $as_xml_object = false) {
 	// Create outline elements
 	$body = $opml->addChild('body');
 	foreach ($array['body'] as $outline) {
-		libopml_render_outline($body, $outline);
+		libopml_render_outline($body, $outline, $strict);
 	}
 
 	// And return the final result

+ 12 - 2
lib/lib_rss.php

@@ -180,7 +180,7 @@ function sanitizeHTML($data, $base = '') {
 function get_content_by_parsing ($url, $path) {
 	require_once (LIB_PATH . '/lib_phpQuery.php');
 
-	syslog(LOG_INFO, 'FreshRSS GET ' . $url);
+	Minz_Log::notice('FreshRSS GET ' . url_remove_credentials($url));
 	$html = file_get_contents ($url);
 
 	if ($html) {
@@ -248,7 +248,7 @@ function listUsers() {
  * @return a Minz_Configuration object, null if the configuration cannot be loaded.
  */
 function get_user_configuration($username) {
-	$namespace = time() . '_user_' . $username;
+	$namespace = 'user_' . $username;
 	try {
 		Minz_Configuration::register($namespace,
 		                             join_path(USERS_PATH, $username, 'config.php'),
@@ -429,3 +429,13 @@ function array_push_unique(&$array, $value) {
 function array_remove(&$array, $value) {
 	$array = array_diff($array, array($value));
 }
+
+
+/**
+ * Sanitize a URL by removing HTTP credentials.
+ * @param $url the URL to sanitize.
+ * @return the same URL without HTTP credentials.
+ */
+function url_remove_credentials($url) {
+	return preg_replace('/[^\/]*:[^:]*@/', '', $url);
+}

+ 14 - 7
p/f.php

@@ -4,6 +4,7 @@ require('../constants.php');
 
 include(LIB_PATH . '/Favicon/Favicon.php');
 include(LIB_PATH . '/Favicon/DataAccess.php');
+require(LIB_PATH . '/http-conditional.php');
 
 
 $favicons_dir = DATA_PATH . '/favicons/';
@@ -46,10 +47,13 @@ function download_favicon($website, $dest) {
 function show_default_favicon() {
 	global $default_favicon;
 
-	header('HTTP/1.1 404 Not Found');
-	header('Content-Type: image/ico');
-	readfile($default_favicon);
-	die();
+	header('Content-Type: image/x-icon');
+	header('Content-Disposition: inline; filename="default_favicon.ico"');
+
+	$default_mtime = @filemtime($default_favicon);
+	if (!httpConditional($default_mtime, 2592000, 2)) {
+		readfile($default_favicon);
+	}
 }
 
 
@@ -64,19 +68,22 @@ $ico = $favicons_dir . $id . '.ico';
 $ico_mtime = @filemtime($ico);
 $txt_mtime = @filemtime($txt);
 
-if (($ico_mtime == false) || ($txt_mtime > $ico_mtime)) {
+
+if ($ico_mtime == false || $txt_mtime > $ico_mtime) {
 	if ($txt_mtime == false) {
 		show_default_favicon();
+		return;
 	}
 
+	// no ico file or we should download a new one.
 	$url = file_get_contents($txt);
 	if (!download_favicon($url, $ico)) {
+		// Download failed, show the default favicon
 		show_default_favicon();
+		return;
 	}
 }
 
-require(LIB_PATH . '/http-conditional.php');
-
 header('Content-Type: image/x-icon');
 header('Content-Disposition: inline; filename="' . $id . '.ico"');
 

+ 7 - 1
p/scripts/main.js

@@ -154,6 +154,9 @@ function mark_read(active, only_not_read) {
 		incUnreadsFeed(active, feed_id, inc);
 		faviconNbUnread();
 
+		pending_feeds.splice(index_pending, 1);
+	}).fail(function (data) {
+		openNotification(i18n.notif_request_failed, 'bad');
 		pending_feeds.splice(index_pending, 1);
 	});
 }
@@ -209,6 +212,9 @@ function mark_favorite(active) {
 			}
 		}
 
+		pending_feeds.splice(index_pending, 1);
+	}).fail(function (data) {
+		openNotification(i18n.notif_request_failed, 'bad');
 		pending_feeds.splice(index_pending, 1);
 	});
 }
@@ -513,7 +519,7 @@ function init_column_categories() {
 		if ($(this).nextAll('.dropdown-menu').length === 0) {
 			var feed_id = $(this).closest('.item').attr('id').substr(2),
 				feed_web = $(this).data('fweb'),
-				template = $('#feed_config_template').html().replace(/!!!!!!/g, feed_id).replace('http://example.net/', feed_web);
+				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);
 		}
 	});

+ 1 - 1
p/themes/BlueLagoon/README.md

@@ -1,7 +1,7 @@
 Blue Lagoon
 =======
 
-**C'est un cocktail (bis)! C'est la version plus "fresh" de [Screwdriver](https://github.com/misterair/Screwdriver). C'est... c'est... un thème pour l'agrégateur de flux RSS [FreshRSS](https://github.com/marienfressinaud/FreshRSS/)**
+**C'est un cocktail (bis)! C'est la version plus "fresh" de [Screwdriver](https://github.com/misterair/Screwdriver). C'est... c'est... un thème pour l'agrégateur de flux RSS [FreshRSS](https://github.com/FreshRSS/FreshRSS/)**
 
 
 En toute modestie, ce thème tue du Nyan Cat.

+ 6 - 0
p/themes/Dark/dark.css

@@ -879,6 +879,12 @@ a.btn {
 	border-radius: 3px;
 }
 
+/*=== Slider */
+#slider {
+	background-color: #1c1c1c;
+	border-left: 1px solid #666;
+}
+
 /*=== DIVERS */
 /*===========*/
 .aside.aside_feed .nav-form input,

+ 1 - 1
p/themes/Screwdriver/README.md

@@ -1,7 +1,7 @@
 Screwdriver 
 =======
 
-**C'est un cocktail! C'est chaud mais "fresh" à la fois. C'est... c'est... un thème pour l'agrégateur de flux RSS<a href="https://github.com/marienfressinaud/FreshRSS/" target="blank">FreshRSS</a>!!**
+**C'est un cocktail! C'est chaud mais "fresh" à la fois. C'est... c'est... un thème pour l'agrégateur de flux RSS<a href="https://github.com/FreshRSS/FreshRSS/" target="blank">FreshRSS</a>!!**
 En toute modestie, ce thème tue du chaton.
 
 ![screenshot](https://github.com/misterair/Screwdriver/blob/master/screenshot.png)

+ 1 - 1
p/themes/base-theme/README.md

@@ -8,5 +8,5 @@ A base theme for [FreshRSS](http://freshrss.org)
 3. Choose your new theme in FreshRSS configuration
 4. Enjoy your wonderful theme!
 
-Don't hesitate to share your theme with us [on Github](https://github.com/marienfressinaud/FreshRSS/issues) :)
+Don't hesitate to share your theme with us [on Github](https://github.com/FreshRSS/FreshRSS/issues) :)
 

+ 2 - 1
p/themes/base-theme/template.css

@@ -75,6 +75,7 @@ input {
 }
 textarea,
 input[type="file"],
+input.long,
 input.extend:focus {
 	width: 300px;
 }
@@ -485,7 +486,7 @@ a.btn {
 	text-decoration: none;
 }
 .flux .item.date {
-	width: 145px;
+	width: 155px;
 	text-align: right;
 	overflow: hidden;
 }

+ 32 - 0
tests/app/Models/CategoryTest.php

@@ -0,0 +1,32 @@
+<?php
+
+class FreshRSS_CategoryTest extends \PHPUnit_Framework_TestCase {
+
+	public function test__construct_whenNoParameters_createsObjectWithDefaultValues() {
+		$category = new FreshRSS_Category();
+		$this->assertEquals(0, $category->id());
+		$this->assertEquals('', $category->name());
+	}
+
+	/**
+	 * @param string $input
+	 * @param string $expected
+	 * @dataProvider provideValidNames
+	 */
+	public function test_name_whenValidValue_storesModifiedValue($input, $expected) {
+		$category = new FreshRSS_Category($input);
+		$this->assertEquals($expected, $category->name());
+	}
+
+	public function provideValidNames() {
+		return array(
+		    array('', ''),
+		    array('this string does not need trimming', 'this string does not need trimming'),
+		    array('  this string needs trimming on left', 'this string needs trimming on left'),
+		    array('this string needs trimming on right  ', 'this string needs trimming on right'),
+		    array('  this string needs trimming on both ends  ', 'this string needs trimming on both ends'),
+		    array(str_repeat('This string needs to be shortened because its length is way too long. ', 4), str_repeat('This string needs to be shortened because its length is way too long. ', 3) . 'This string needs to be shortened because its'),
+		);
+	}
+
+}

+ 7 - 0
tests/bootstrap.php

@@ -0,0 +1,7 @@
+<?php
+
+error_reporting(E_ALL);
+ini_set('display_errors', 1);
+
+require('../constants.php');
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader

+ 13 - 0
tests/phpunit.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="bootstrap.php">
+	<filter>
+		<whitelist>
+			<directory suffix=".php">../app</directory>
+		</whitelist>
+	</filter>
+	<testsuites>
+		<testsuite name="FreshRSS">
+			<directory>app</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>