Преглед изворни кода

Merge branch 'dev' into beta

Conflicts:
	README.fr.md
	README.md
Marien Fressinaud пре 11 година
родитељ
комит
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)
 ## 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
 * Site officiel : http://freshrss.org
 * Démo : http://demo.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)
 ![Logo de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
 
 
 # Note sur les branches
 # Note sur les branches
 **Ce logiciel est encore en développement !** Veuillez vous assurer d'utiliser la branche qui vous correspond :
 **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
 # Disclaimer
 Cette application a été développée pour s’adapter à des besoins personnels et non professionnels.
 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 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.
 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
 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
 # Pré-requis
 * Serveur modeste, par exemple sous Linux ou Windows
 * 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)
 ![Capture d’écran de FreshRSS](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
 
 
 # Installation
 # 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/`)
 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/`
 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
 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
 * Official website: http://freshrss.org
 * Demo: http://demo.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)
 ![FreshRSS logo](http://marienfressinaud.fr/data/images/freshrss/freshrss_title.png)
 
 
 # Note on branches
 # Note on branches
 **This application is still in development!** Please use the branch that suits your needs:
 **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
 # Disclaimer
 This application was developed to fulfill personal needs not professional needs.
 This application was developed to fulfill personal needs not professional needs.
 There is no guarantee neither on its security nor its proper functioning.
 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.
 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
 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
 # Requirements
 * Light server running Linux or Windows
 * 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)
 * 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)
 	* 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)
 	* 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+
 * A recent browser like Firefox 4+, Chrome, Opera, Safari, Internet Explorer 9+
 	* Works on mobile
 	* Works on mobile
 
 
 ![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
 ![FreshRSS screenshot](http://marienfressinaud.fr/data/images/freshrss/freshrss_default-design.png)
 
 
 # Installation
 # 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)
 2. Dump the application on your server (expose only the `./p/` folder)
 3. Add write access on `./data/` folder to the webserver user
 3. Add write access on `./data/` folder to the webserver user
 4. Access FreshRSS with your browser and follow the installation process
 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
 # Advices
 * For a better security, expose only the `./p/` folder on the web.
 * 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
 # Backup
 * You need to keep `./data/config.php`, `./data/*_user.php` and `./data/persona/` files
 * 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.
 	 * 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() {
 	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) {
 		switch ($code_int) {
+		case 200 :
+			header('HTTP/1.1 200 OK');
+			break;
 		case 403:
 		case 403:
+			header('HTTP/1.1 403 Forbidden');
 			$this->view->code = 'Error 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;
 			break;
 		case 500:
 		case 500:
+			header('HTTP/1.1 500 Internal Server Error');
 			$this->view->code = 'Error 500 - Internal Server Error';
 			$this->view->code = 'Error 500 - Internal Server Error';
 			break;
 			break;
 		case 503:
 		case 503:
+			header('HTTP/1.1 503 Service Unavailable');
 			$this->view->code = 'Error 503 - Service Unavailable';
 			$this->view->code = 'Error 503 - Service Unavailable';
 			break;
 			break;
+		case 404:
 		default:
 		default:
+			header('HTTP/1.1 404 Not Found');
 			$this->view->code = 'Error 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 . ' · ');
 		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) {
 	private function importOpml($opml_file) {
 		$opml_array = array();
 		$opml_array = array();
 		try {
 		try {
-			$opml_array = libopml_parse_string($opml_file);
+			$opml_array = libopml_parse_string($opml_file, false);
 		} catch (LibOPML_Exception $e) {
 		} catch (LibOPML_Exception $e) {
 			Minz_Log::warning($e->getMessage());
 			Minz_Log::warning($e->getMessage());
 			return true;
 			return true;

+ 4 - 3
app/Controllers/subscriptionController.php

@@ -102,13 +102,14 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 
 
 			invalidateHttpCache();
 			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->_category($cat);
 				$this->view->feed->faviconPrepare();
 				$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 {
 			} 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)) {
 		} elseif (file_exists(UPDATE_FILENAME)) {
 			// There is an update file to apply!
 			// 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->update_to_apply = true;
 			$this->view->message = array(
 			$this->view->message = array(
 				'status' => 'good',
 				'status' => 'good',

+ 5 - 7
app/Models/ConfigurationSetter.php

@@ -153,11 +153,11 @@ class FreshRSS_ConfigurationSetter {
 	}
 	}
 
 
 	private function _shortcuts(&$data, $values) {
 	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) {
 	private function _sort_order(&$data, $value) {
@@ -210,9 +210,7 @@ class FreshRSS_ConfigurationSetter {
 
 
 	private function _mark_when(&$data, $values) {
 	private function _mark_when(&$data, $values) {
 		foreach ($values as $key => $value) {
 		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);
 					$subscribe_url = $feed->subscribe_url(true);
 				}
 				}
 
 
+				$clean_url = url_remove_credentials($subscribe_url);
 				if ($subscribe_url !== null && $subscribe_url !== $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)) {
 				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
 					$this->loadEntries($feed);	// et on charge les articles du flux
 				} else {
 				} else {
-					syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url);
+					Minz_Log::notice('FreshRSS use cache for ' . $clean_url);
 					$this->entries = array();
 					$this->entries = array();
 				}
 				}
 
 

+ 10 - 5
app/actualize_script.php

@@ -21,8 +21,10 @@ $_GET['force'] = true;
 $_SERVER['HTTP_HOST'] = '';
 $_SERVER['HTTP_HOST'] = '';
 
 
 
 
+$log_file = join_path(USERS_PATH, '_', 'log.txt');
+
+
 $app = new FreshRSS();
 $app = new FreshRSS();
-$app->init();
 
 
 $system_conf = Minz_Configuration::get('system');
 $system_conf = Minz_Configuration::get('system');
 $system_conf->auth_type = 'none';  // avoid necessity to be logged in (not saved!)
 $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) {
 foreach ($users as $user) {
 	if (($user !== $system_conf->default_user) &&
 	if (($user !== $system_conf->default_user) &&
 			(FreshRSS_UserDAO::mtime($user) < $min_last_activity)) {
 			(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')) {
 		if (defined('STDOUT')) {
 			fwrite(STDOUT, 'FreshRSS skip inactive user ' . $user . "\n");	//Unbuffered
 			fwrite(STDOUT, 'FreshRSS skip inactive user ' . $user . "\n");	//Unbuffered
 		}
 		}
 		continue;
 		continue;
 	}
 	}
-	syslog(LOG_INFO, 'FreshRSS actualize ' . $user);
+	Minz_Log::notice('FreshRSS actualize ' . $user, $log_file);
 	if (defined('STDOUT')) {
 	if (defined('STDOUT')) {
 		fwrite(STDOUT, 'Actualize ' . $user . "...\n");	//Unbuffered
 		fwrite(STDOUT, 'Actualize ' . $user . "...\n");	//Unbuffered
 	}
 	}
@@ -56,12 +58,15 @@ foreach ($users as $user) {
 
 
 
 
 	Minz_Session::_param('currentUser', $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();
 	FreshRSS_Auth::giveAccess();
+	$app->init();
 	$app->run();
 	$app->run();
 
 
 
 
 	if (!invalidateHttpCache()) {
 	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')) {
 		if (defined('STDERR')) {
 			fwrite(STDERR, 'Write access problem in ' . join_path(USERS_PATH, $user, 'log.txt') . "\n");
 			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')) {
 if (defined('STDOUT')) {
 	fwrite(STDOUT, 'Done.' . "\n");
 	fwrite(STDOUT, 'Done.' . "\n");
 	$end_date = date_create('now');
 	$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>',
 		'email_persona' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Language',
 		'language' => 'Language',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
+		'password_format' => 'At least 7 characters',
 		'title' => 'Manage users',
 		'title' => 'Manage users',
 		'user_list' => 'List of users',
 		'user_list' => 'List of users',
 		'username' => 'Username',
 		'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>',
 		'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_api' => 'Password API<br /><small>(e.g., for mobile apps)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
+		'password_format' => 'At least 7 characters',
 		'title' => 'Profile',
 		'title' => 'Profile',
 	),
 	),
 	'reading' => array(
 	'reading' => array(

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

@@ -95,12 +95,16 @@ return array(
 		'category_empty' => 'Empty category',
 		'category_empty' => 'Empty category',
 		'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
 		'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!',
 		'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.',
 		'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',
 		'should_be_activated' => 'JavaScript must be enabled',
 	),
 	),
 	'lang' => array(
 	'lang' => array(
+		'de' => 'Deutsch',
 		'en' => 'English',
 		'en' => 'English',
 		'fr' => 'Français',
 		'fr' => 'Français',
 	),
 	),

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

@@ -8,8 +8,7 @@ return array(
 		'credits' => 'Credits',
 		'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.',
 		'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.',
 		'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',
 		'license' => 'License',
 		'project_website' => 'Project website',
 		'project_website' => 'Project website',
 		'title' => 'About',
 		'title' => 'About',

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

@@ -12,6 +12,7 @@ return array(
 		'http' => 'HTTP (for advanced users with HTTPS)',
 		'http' => 'HTTP (for advanced users with HTTPS)',
 		'none' => 'None (dangerous)',
 		'none' => 'None (dangerous)',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
+		'password_format' => 'At least 7 characters',
 		'persona' => 'Mozilla Persona (modern, requires JavaScript)',
 		'persona' => 'Mozilla Persona (modern, requires JavaScript)',
 		'type' => 'Authentication method',
 		'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>',
 		'email_persona' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Langue',
 		'language' => 'Langue',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+		'password_format' => '7 caractères minimum',
 		'title' => 'Gestion des utilisateurs',
 		'title' => 'Gestion des utilisateurs',
 		'user_list' => 'Liste des utilisateurs',
 		'user_list' => 'Liste des utilisateurs',
 		'username' => 'Nom d’utilisateur',
 		'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>',
 		'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_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_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+		'password_format' => '7 caractères minimum',
 		'title' => 'Profil',
 		'title' => 'Profil',
 	),
 	),
 	'reading' => array(
 	'reading' => array(

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

@@ -95,12 +95,16 @@ return array(
 		'category_empty' => 'Catégorie vide',
 		'category_empty' => 'Catégorie vide',
 		'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
 		'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 !',
 		'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.',
 		'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é.',
 		'should_be_activated' => 'Le JavaScript doit être activé.',
 	),
 	),
 	'lang' => array(
 	'lang' => array(
+		'de' => 'Deutsch',
 		'en' => 'English',
 		'en' => 'English',
 		'fr' => 'Français',
 		'fr' => 'Français',
 	),
 	),

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

@@ -8,8 +8,7 @@ return array(
 		'credits' => 'Crédits',
 		'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.',
 		'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.',
 		'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',
 		'license' => 'Licence',
 		'project_website' => 'Site du projet',
 		'project_website' => 'Site du projet',
 		'title' => 'À propos',
 		'title' => 'À propos',

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

@@ -12,6 +12,7 @@ return array(
 		'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
 		'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
 		'none' => 'Aucune (dangereux)',
 		'none' => 'Aucune (dangereux)',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
+		'password_format' => '7 caractères minimum',
 		'persona' => 'Mozilla Persona (moderne, requiert JavaScript)',
 		'persona' => 'Mozilla Persona (moderne, requiert JavaScript)',
 		'type' => 'Méthode d’authentification',
 		'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"' : ''; ?> />
 					<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>
 					<a class="btn toggle-password" data-toggle="passwordPlain"><?php echo FreshRSS_Themes::icon('key'); ?></a>
 				</div>
 				</div>
+				<?php echo _i('help'); ?> <?php echo _t('install.auth.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 			</div>
 			</div>
 		</div>
 		</div>

+ 5 - 5
app/layout/aside_feed.phtml

@@ -74,20 +74,20 @@
 <script id="feed_config_template" type="text/html">
 <script id="feed_config_template" type="text/html">
 	<ul class="dropdown-menu">
 	<ul class="dropdown-menu">
 		<li class="dropdown-close"><a href="#close">❌</a></li>
 		<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()) { ?>
 		<?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 } ?>
 		<?php } ?>
 		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li>
 		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li>
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<li class="separator"></li>
 		<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">
 		<li class="item">
 			<?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>
 			<?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>
 			<button class="read_all as-link <?php echo $confirm; ?>"
 			<button class="read_all as-link <?php echo $confirm; ?>"
 			        form="mark-read-aside"
 			        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>
 			        type="submit"><?php echo _t('gen.action.mark_read'); ?></button>
 		</li>
 		</li>
 		<?php } ?>
 		<?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) { ?>
 		<?php if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::$system_conf->allow_anonymous) { ?>
 		<form action="<?php echo _url('index', 'index'); ?>" method="get">
 		<form action="<?php echo _url('index', 'index'); ?>" method="get">
 			<div class="stick">
 			<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 $get = Minz_Request::param('get', ''); ?>
 				<?php if ($get != '') { ?>
 				<?php if ($get != '') { ?>

+ 1 - 2
app/layout/nav_menu.phtml

@@ -156,8 +156,7 @@
 
 
 	<div class="item search">
 	<div class="item search">
 		<form action="<?php echo _url('index', 'index'); ?>" method="get">
 		<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 $get = Minz_Request::param('get', ''); ?>
 			<?php if($get != '') { ?>
 			<?php if($get != '') { ?>

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

@@ -114,7 +114,7 @@
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="close_dropdown_shortcut"><?php echo _t('conf.shortcut.close_dropdown'); ?></label>
 			<label class="group-name" for="close_dropdown_shortcut"><?php echo _t('conf.shortcut.close_dropdown'); ?></label>
 			<div class="group-controls">
 			<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>
 		</div>
 		</div>
 
 

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

@@ -35,20 +35,20 @@ echo 'var context={',
 "},\n";
 "},\n";
 
 
 echo 'shortcuts={',
 echo 'shortcuts={',
-	'mark_read:"', $s['mark_read'], '",',
-	'mark_favorite:"', $s['mark_favorite'], '",',
-	'go_website:"', $s['go_website'], '",',
-	'prev_entry:"', $s['prev_entry'], '",',
-	'next_entry:"', $s['next_entry'], '",',
-	'first_entry:"', $s['first_entry'], '",',
-	'last_entry:"', $s['last_entry'], '",',
-	'collapse_entry:"', $s['collapse_entry'], '",',
-	'load_more:"', $s['load_more'], '",',
-	'auto_share:"', $s['auto_share'], '",',
-	'focus_search:"', $s['focus_search'], '",',
-	'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";
 "},\n";
 
 
 echo 'url={',
 echo 'url={',
@@ -60,8 +60,9 @@ echo 'url={',
 
 
 echo 'i18n={',
 echo 'i18n={',
 	'confirmation_default:"', _t('gen.js.confirm_action'), '",',
 	'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'), '"',
 	'category_empty:"', _t('gen.js.category_empty'), '"',
 "},\n";
 "},\n";
 
 

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

@@ -7,11 +7,8 @@
 		<dt><?php echo _t('index.about.project_website'); ?></dt>
 		<dt><?php echo _t('index.about.project_website'); ?></dt>
 		<dd><a href="<?php echo FRESHRSS_WEBSITE; ?>"><?php echo FRESHRSS_WEBSITE; ?></a></dd>
 		<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>
 		<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>
 		<dt><?php echo _t('index.about.license'); ?></dt>
 		<dd><?php echo _t('index.about.agpl3'); ?></dd>
 		<dd><?php echo _t('index.about.agpl3'); ?></dd>

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

@@ -13,7 +13,7 @@
 <?php
 <?php
 	$url_base = array(
 	$url_base = array(
 		'c' => 'index',
 		'c' => 'index',
-		'a' => 'index',
+		'a' => 'normal',
 		'params' => Minz_Request::params()
 		'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">
 	<form id="add_rss" method="post" action="<?php echo _url('feed', 'add'); ?>" autocomplete="off">
 		<div class="stick">
 		<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 class="dropdown">
 				<div id="dropdown-cat" class="dropdown-target"></div>
 				<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,}" />
 					<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>
 					<a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				</div>
+				<?php echo _i('help'); ?> <?php echo _t('admin.user.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 			</div>
 			</div>
 		</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" '; ?>/>
 					<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>
 					<a class="btn toggle-password" data-toggle="passwordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				</div>
+				<?php echo _i('help'); ?> <?php echo _t('conf.profile.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
 			</div>
 			</div>
 		</div>
 		</div>

+ 6 - 9
data/shares.php

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

+ 0 - 7
lib/Minz/Configuration.php

@@ -16,16 +16,9 @@ class Minz_Configuration {
 	 * @param $config_filename the filename of the configuration
 	 * @param $config_filename the filename of the configuration
 	 * @param $default_filename a filename containing default values for the configuration
 	 * @param $default_filename a filename containing default values for the configuration
 	 * @param $configuration_setter an optional helper to set values in 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,
 	public static function register($namespace, $config_filename, $default_filename = null,
 	                                $configuration_setter = 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(
 		self::$config_list[$namespace] = new Minz_Configuration(
 			$namespace, $config_filename, $default_filename, $configuration_setter
 			$namespace, $config_filename, $default_filename, $configuration_setter
 		);
 		);

+ 5 - 34
lib/Minz/Error.php

@@ -23,42 +23,13 @@ class Minz_Error {
 		$logs = self::processLogs ($logs);
 		$logs = self::processLogs ($logs);
 		$error_filename = APP_PATH . '/Controllers/errorController.php';
 		$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)) {
 		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 {
 		} else {
 			echo '<h1>An error occured</h1>' . "\n";
 			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) {
 		foreach ($list_potential_extensions as $ext_dir) {
 			$ext_pathname = EXTENSIONS_PATH . '/' . $ext_dir;
 			$ext_pathname = EXTENSIONS_PATH . '/' . $ext_dir;
+			if (!is_dir($ext_pathname)) {
+				continue;
+			}
 			$metadata_filename = $ext_pathname . '/' . self::$ext_metaname;
 			$metadata_filename = $ext_pathname . '/' . self::$ext_metaname;
 
 
 			// Try to load metadata file.
 			// Try to load metadata file.
@@ -111,7 +114,7 @@ class Minz_ExtensionManager {
 		$entry_point_filename = $info['path'] . '/' . self::$ext_entry_point;
 		$entry_point_filename = $info['path'] . '/' . self::$ext_entry_point;
 		$ext_class_name = $info['entrypoint'] . 'Extension';
 		$ext_class_name = $info['entrypoint'] . 'Extension';
 
 
-		include($entry_point_filename);
+		include_once($entry_point_filename);
 
 
 		// Test if the given extension class exists.
 		// Test if the given extension class exists.
 		if (!class_exists($ext_class_name)) {
 		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
 	 * @param $l la durée de vie
 	 */
 	 */
 	public static function keepCookie($l) {
 	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);
 		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;
 		return $url_string;
 	}
 	}
-	
+
 	/**
 	/**
 	 * Construit l'URI d'une URL
 	 * Construit l'URI d'une URL
 	 * @param l'url sous forme de tableau
 	 * @param l'url sous forme de tableau
 	 * @param $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
 	 * @param $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
 	 * @return l'uri sous la forme ?key=value&key2=value2
 	 * @return l'uri sous la forme ?key=value&key2=value2
 	 */
 	 */
-	private static function printUri ($url, $encodage) {
+	private static function printUri($url, $encodage) {
 		$uri = '';
 		$uri = '';
 		$separator = '?';
 		$separator = '?';
-		
-		if($encodage == 'html') {
+
+		if ($encodage === 'html') {
 			$and = '&amp;';
 			$and = '&amp;';
 		} else {
 		} else {
 			$and = '&';
 			$and = '&';
 		}
 		}
-		
-		if (isset ($url['c'])
-		 && $url['c'] != Minz_Request::defaultControllerName ()) {
+
+		if (isset($url['c'])
+		 && $url['c'] != Minz_Request::defaultControllerName()) {
 			$uri .= $separator . 'c=' . $url['c'];
 			$uri .= $separator . 'c=' . $url['c'];
 			$separator = $and;
 			$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'];
 			$uri .= $separator . 'a=' . $url['a'];
 			$separator = $and;
 			$separator = $and;
 		}
 		}
-		
-		if (isset ($url['params'])) {
+
+		if (isset($url['params'])) {
 			foreach ($url['params'] as $key => $param) {
 			foreach ($url['params'] as $key => $param) {
-				$uri .= $separator . $key . '=' . $param;
+				$uri .= $separator . urlencode($key) . '=' . urlencode($param);
 				$separator = $and;
 				$separator = $and;
 			}
 			}
 		}
 		}
-		
+
 		return $uri;
 		return $uri;
 	}
 	}
-	
+
 	/**
 	/**
 	 * Vérifie que les éléments du tableau représentant une url soit ok
 	 * 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)
 	 * @param l'url sous forme de tableau (sinon renverra directement $url)
@@ -91,7 +91,7 @@ class Minz_Url {
 	 */
 	 */
 	public static function checkUrl ($url) {
 	public static function checkUrl ($url) {
 		$url_checked = $url;
 		$url_checked = $url;
-		
+
 		if (is_array ($url)) {
 		if (is_array ($url)) {
 			if (!isset ($url['c'])) {
 			if (!isset ($url['c'])) {
 				$url_checked['c'] = Minz_Request::defaultControllerName ();
 				$url_checked['c'] = Minz_Request::defaultControllerName ();
@@ -103,7 +103,7 @@ class Minz_Url {
 				$url_checked['params'] = array ();
 				$url_checked['params'] = array ();
 			}
 			}
 		}
 		}
-		
+
 		return $url_checked;
 		return $url_checked;
 	}
 	}
 }
 }

+ 2 - 2
lib/SimplePie/SimplePie.php

@@ -1529,11 +1529,11 @@ class SimplePie
 					{	//FreshRSS
 					{	//FreshRSS
 						$md5 = $this->cleanMd5($file->body);
 						$md5 = $this->cleanMd5($file->body);
 						if ($this->data['md5'] === $md5) {
 						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();
 							$cache->touch();
 							return true;	//Content unchanged even though server did not send a 304
 							return true;	//Content unchanged even though server did not send a 304
 						} else {
 						} 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;
 							$this->data['md5'] = $md5;
 						}
 						}
 					}
 					}

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

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

+ 102 - 26
lib/lib_opml.php

@@ -1,11 +1,19 @@
 <?php
 <?php
 
 
-/* *
+/**
  * lib_opml is a free library to manage OPML format in 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:
  * Usages:
  * > include('lib_opml.php');
  * > include('lib_opml.php');
@@ -23,21 +31,44 @@
  * > echo $opml_string;
  * > echo $opml_string;
  * > print_r($opml_object);
  * > 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
  * If parsing fails for any reason (e.g. not an XML string, does not match with
  * the specifications), a LibOPML_Exception is raised.
  * 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 {}
 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(
 define('HEAD_ELEMENTS', serialize(array(
 	'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
 	'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
 	'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop',
 	'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();
 	$outline = array();
 
 
 	// An outline may contain any kind of attributes but "text" attribute is
 	// 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(
 		throw new LibOPML_Exception(
 			'Outline does not contain any text attribute'
 			'Outline does not contain any text attribute'
 		);
 		);
@@ -68,7 +108,7 @@ function libopml_parse_outline($outline_xml) {
 	foreach ($outline_xml->children() as $key => $value) {
 	foreach ($outline_xml->children() as $key => $value) {
 		// An outline may contain any number of outline children
 		// An outline may contain any number of outline children
 		if ($key === 'outline') {
 		if ($key === 'outline') {
-			$outline['@outlines'][] = libopml_parse_outline($value);
+			$outline['@outlines'][] = libopml_parse_outline($value, $strict);
 		} else {
 		} else {
 			throw new LibOPML_Exception(
 			throw new LibOPML_Exception(
 				'Body can contain only outline elements'
 				'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 = new DOMDocument();
 	$dom->recover = true;
 	$dom->recover = true;
 	$dom->strictErrorChecking = false;
 	$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
 	// First, we get all "head" elements. Head is required but its sub-elements
 	// are optional.
 	// are optional.
-	// TODO: test head exists!
 	foreach ($opml->head->children() as $key => $value) {
 	foreach ($opml->head->children() as $key => $value) {
 		if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
 		if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
 			$array['head'][$key] = (string)$value;
 			$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
 	// Then, we get body oulines. Body must contain at least one outline
 	// element.
 	// element.
 	$at_least_one_outline = false;
 	$at_least_one_outline = false;
-	// TODO: test body exists!
 	foreach ($opml->body->children() as $key => $value) {
 	foreach ($opml->body->children() as $key => $value) {
 		if ($key === 'outline') {
 		if ($key === 'outline') {
 			$at_least_one_outline = true;
 			$at_least_one_outline = true;
-			$array['body'][] = libopml_parse_outline($value);
+			$array['body'][] = libopml_parse_outline($value, $strict);
 		} else {
 		} else {
 			throw new LibOPML_Exception(
 			throw new LibOPML_Exception(
 				'Body can contain only outline elements'
 				'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);
 	$file_content = file_get_contents($filename);
 
 
 	if ($file_content === false) {
 	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!
 	// Outline MUST be an array!
 	if (!is_array($outline)) {
 	if (!is_array($outline)) {
 		throw new LibOPML_Exception(
 		throw new LibOPML_Exception(
@@ -165,7 +230,7 @@ function libopml_render_outline($parent_elt, $outline) {
 		// outline elements.
 		// outline elements.
 		if ($key === '@outlines' && is_array($value)) {
 		if ($key === '@outlines' && is_array($value)) {
 			foreach ($value as $outline_child) {
 			foreach ($value as $outline_child) {
-				libopml_render_outline($outline_elt, $outline_child);
+				libopml_render_outline($outline_elt, $outline_child, $strict);
 			}
 			}
 		} elseif (is_array($value)) {
 		} elseif (is_array($value)) {
 			throw new LibOPML_Exception(
 			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(
 		throw new LibOPML_Exception(
 			'You must define at least a text element for all outlines'
 			'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
 	// Create head element. $array['head'] is optional but head element will
 	// exist in the final XML object.
 	// exist in the final XML object.
@@ -218,7 +294,7 @@ function libopml_render($array, $as_xml_object = false) {
 	// Create outline elements
 	// Create outline elements
 	$body = $opml->addChild('body');
 	$body = $opml->addChild('body');
 	foreach ($array['body'] as $outline) {
 	foreach ($array['body'] as $outline) {
-		libopml_render_outline($body, $outline);
+		libopml_render_outline($body, $outline, $strict);
 	}
 	}
 
 
 	// And return the final result
 	// 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) {
 function get_content_by_parsing ($url, $path) {
 	require_once (LIB_PATH . '/lib_phpQuery.php');
 	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);
 	$html = file_get_contents ($url);
 
 
 	if ($html) {
 	if ($html) {
@@ -248,7 +248,7 @@ function listUsers() {
  * @return a Minz_Configuration object, null if the configuration cannot be loaded.
  * @return a Minz_Configuration object, null if the configuration cannot be loaded.
  */
  */
 function get_user_configuration($username) {
 function get_user_configuration($username) {
-	$namespace = time() . '_user_' . $username;
+	$namespace = 'user_' . $username;
 	try {
 	try {
 		Minz_Configuration::register($namespace,
 		Minz_Configuration::register($namespace,
 		                             join_path(USERS_PATH, $username, 'config.php'),
 		                             join_path(USERS_PATH, $username, 'config.php'),
@@ -429,3 +429,13 @@ function array_push_unique(&$array, $value) {
 function array_remove(&$array, $value) {
 function array_remove(&$array, $value) {
 	$array = array_diff($array, 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/Favicon.php');
 include(LIB_PATH . '/Favicon/DataAccess.php');
 include(LIB_PATH . '/Favicon/DataAccess.php');
+require(LIB_PATH . '/http-conditional.php');
 
 
 
 
 $favicons_dir = DATA_PATH . '/favicons/';
 $favicons_dir = DATA_PATH . '/favicons/';
@@ -46,10 +47,13 @@ function download_favicon($website, $dest) {
 function show_default_favicon() {
 function show_default_favicon() {
 	global $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);
 $ico_mtime = @filemtime($ico);
 $txt_mtime = @filemtime($txt);
 $txt_mtime = @filemtime($txt);
 
 
-if (($ico_mtime == false) || ($txt_mtime > $ico_mtime)) {
+
+if ($ico_mtime == false || $txt_mtime > $ico_mtime) {
 	if ($txt_mtime == false) {
 	if ($txt_mtime == false) {
 		show_default_favicon();
 		show_default_favicon();
+		return;
 	}
 	}
 
 
+	// no ico file or we should download a new one.
 	$url = file_get_contents($txt);
 	$url = file_get_contents($txt);
 	if (!download_favicon($url, $ico)) {
 	if (!download_favicon($url, $ico)) {
+		// Download failed, show the default favicon
 		show_default_favicon();
 		show_default_favicon();
+		return;
 	}
 	}
 }
 }
 
 
-require(LIB_PATH . '/http-conditional.php');
-
 header('Content-Type: image/x-icon');
 header('Content-Type: image/x-icon');
 header('Content-Disposition: inline; filename="' . $id . '.ico"');
 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);
 		incUnreadsFeed(active, feed_id, inc);
 		faviconNbUnread();
 		faviconNbUnread();
 
 
+		pending_feeds.splice(index_pending, 1);
+	}).fail(function (data) {
+		openNotification(i18n.notif_request_failed, 'bad');
 		pending_feeds.splice(index_pending, 1);
 		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);
 		pending_feeds.splice(index_pending, 1);
 	});
 	});
 }
 }
@@ -513,7 +519,7 @@ function init_column_categories() {
 		if ($(this).nextAll('.dropdown-menu').length === 0) {
 		if ($(this).nextAll('.dropdown-menu').length === 0) {
 			var feed_id = $(this).closest('.item').attr('id').substr(2),
 			var feed_id = $(this).closest('.item').attr('id').substr(2),
 				feed_web = $(this).data('fweb'),
 				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);
 			$(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
 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.
 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;
 	border-radius: 3px;
 }
 }
 
 
+/*=== Slider */
+#slider {
+	background-color: #1c1c1c;
+	border-left: 1px solid #666;
+}
+
 /*=== DIVERS */
 /*=== DIVERS */
 /*===========*/
 /*===========*/
 .aside.aside_feed .nav-form input,
 .aside.aside_feed .nav-form input,

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

@@ -1,7 +1,7 @@
 Screwdriver 
 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.
 En toute modestie, ce thème tue du chaton.
 
 
 ![screenshot](https://github.com/misterair/Screwdriver/blob/master/screenshot.png)
 ![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
 3. Choose your new theme in FreshRSS configuration
 4. Enjoy your wonderful theme!
 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,
 textarea,
 input[type="file"],
 input[type="file"],
+input.long,
 input.extend:focus {
 input.extend:focus {
 	width: 300px;
 	width: 300px;
 }
 }
@@ -485,7 +486,7 @@ a.btn {
 	text-decoration: none;
 	text-decoration: none;
 }
 }
 .flux .item.date {
 .flux .item.date {
-	width: 145px;
+	width: 155px;
 	text-align: right;
 	text-align: right;
 	overflow: hidden;
 	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>