Bläddra i källkod

Merge branch 'dev' into beta

Marien Fressinaud 10 år sedan
förälder
incheckning
5fbbfdea8a
76 ändrade filer med 1233 tillägg och 282 borttagningar
  1. 29 0
      CHANGELOG.md
  2. 2 1
      README.fr.md
  3. 2 1
      README.md
  4. 12 1
      app/Controllers/authController.php
  5. 72 26
      app/Controllers/feedController.php
  6. 3 3
      app/Controllers/importExportController.php
  7. 3 3
      app/Controllers/subscriptionController.php
  8. 1 1
      app/Controllers/updateController.php
  9. 93 8
      app/Controllers/userController.php
  10. 1 1
      app/Models/CategoryDAO.php
  11. 6 2
      app/Models/ConfigurationSetter.php
  12. 12 5
      app/Models/EntryDAO.php
  13. 149 1
      app/Models/Feed.php
  14. 2 0
      app/SQL/install.sql.mysql.php
  15. 2 0
      app/SQL/install.sql.sqlite.php
  16. 7 0
      app/i18n/cz/admin.php
  17. 5 0
      app/i18n/cz/conf.php
  18. 1 0
      app/i18n/cz/feedback.php
  19. 15 3
      app/i18n/cz/gen.php
  20. 6 0
      app/i18n/cz/install.php
  21. 1 0
      app/i18n/cz/sub.php
  22. 7 0
      app/i18n/de/admin.php
  23. 4 0
      app/i18n/de/conf.php
  24. 1 0
      app/i18n/de/feedback.php
  25. 15 3
      app/i18n/de/gen.php
  26. 6 0
      app/i18n/de/install.php
  27. 1 0
      app/i18n/de/sub.php
  28. 7 0
      app/i18n/en/admin.php
  29. 4 0
      app/i18n/en/conf.php
  30. 1 0
      app/i18n/en/feedback.php
  31. 15 3
      app/i18n/en/gen.php
  32. 6 0
      app/i18n/en/install.php
  33. 1 0
      app/i18n/en/sub.php
  34. 7 0
      app/i18n/fr/admin.php
  35. 4 0
      app/i18n/fr/conf.php
  36. 1 0
      app/i18n/fr/feedback.php
  37. 15 3
      app/i18n/fr/gen.php
  38. 6 0
      app/i18n/fr/install.php
  39. 2 1
      app/i18n/fr/sub.php
  40. 141 41
      app/install.php
  41. 4 0
      app/views/auth/formLogin.phtml
  42. 4 0
      app/views/auth/personaLogin.phtml
  43. 38 0
      app/views/auth/register.phtml
  44. 1 1
      app/views/auth/reset.phtml
  45. 1 1
      app/views/configure/display.phtml
  46. 1 1
      app/views/configure/reading.phtml
  47. 1 1
      app/views/feed/add.phtml
  48. 12 4
      app/views/helpers/feed/update.phtml
  49. 2 2
      app/views/subscription/index.phtml
  50. 28 0
      app/views/user/manage.phtml
  51. 34 3
      app/views/user/profile.phtml
  52. 2 1
      constants.php
  53. 1 0
      data/PubSubHubbub/feeds/.gitignore
  54. 7 0
      data/PubSubHubbub/feeds/README.md
  55. 1 0
      data/PubSubHubbub/keys/.gitignore
  56. 4 0
      data/PubSubHubbub/keys/README.md
  57. 38 7
      data/config.default.php
  58. 1 1
      data/users/_/config.default.php
  59. 11 17
      lib/Minz/Configuration.php
  60. 1 1
      lib/Minz/Extension.php
  61. 4 6
      lib/Minz/ModelPdo.php
  62. 28 17
      lib/Minz/Request.php
  63. 6 1
      lib/Minz/Url.php
  64. 1 0
      lib/Minz/View.php
  65. 78 71
      lib/SimplePie/SimplePie.php
  66. 2 10
      lib/SimplePie/SimplePie/Cache/File.php
  67. 0 1
      lib/SimplePie/SimplePie/Decode/HTML/Entities.php
  68. 7 4
      lib/SimplePie/SimplePie/File.php
  69. 34 8
      lib/SimplePie/SimplePie/Item.php
  70. 1 1
      lib/SimplePie/SimplePie/Locator.php
  71. 4 4
      lib/SimplePie/SimplePie/Misc.php
  72. 3 3
      lib/SimplePie/SimplePie/Parse/Date.php
  73. 2 2
      lib/SimplePie/SimplePie/Registry.php
  74. 2 2
      lib/SimplePie/SimplePie/Sanitize.php
  75. 68 5
      lib/lib_rss.php
  76. 133 0
      p/api/pshb.php

+ 29 - 0
CHANGELOG.md

@@ -1,5 +1,34 @@
 # Changelog
 # Changelog
 
 
+## 2015-07-30 FreshRSS 1.1.2-beta
+
+* Features
+	* Support for PubSubHubbub for instant notifications from compatible Web sites. [#312](https://github.com/FreshRSS/FreshRSS/issues/312)
+	* cURL options to use a proxy for retrieving feeds. [#897](https://github.com/FreshRSS/FreshRSS/issues/897) [#675](https://github.com/FreshRSS/FreshRSS/issues/675)
+	* Allow anonymous users to create an account. [#679](https://github.com/FreshRSS/FreshRSS/issues/679)
+* Security
+	* cURL options to verify or not SSL/TLS certificates (now enabled by default). [#897](https://github.com/FreshRSS/FreshRSS/issues/897) [#502](https://github.com/FreshRSS/FreshRSS/issues/502)
+	* Support for SSL connection to MySQL. [#868](https://github.com/FreshRSS/FreshRSS/issues/868)
+	* Workaround for browsers that have disabled support for `<form autocomplete="off">`. [#880](https://github.com/FreshRSS/FreshRSS/issues/880)
+* UI
+	* Force UTF-8 for responses. [#870](https://github.com/FreshRSS/FreshRSS/issues/870)
+	* Increased pagination limit to 500 articles. [#872](https://github.com/FreshRSS/FreshRSS/issues/872)
+	* Improved UI for installation. [#855](https://github.com/FreshRSS/FreshRSS/issues/855)
+* Misc.
+	* PHP 7 officially supported (~70% speed improvements on early tests). [#889](https://github.com/FreshRSS/FreshRSS/issues/889)
+	* Restore support for PHP 5.2.1+. [#214a5cc](https://github.com/Alkarex/FreshRSS/commit/214a5cc9a4c2b821961bc21f22b4b08e34b5be68) [#894](https://github.com/FreshRSS/FreshRSS/issues/894)
+	* Support for data-src for images of articles retrieved via the full-content module. [#877](https://github.com/FreshRSS/FreshRSS/issues/877)
+	* Add a couple of default feeds for fresh installations. [#886](https://github.com/FreshRSS/FreshRSS/issues/886)
+	* Changed some log visibilities. [#885](https://github.com/FreshRSS/FreshRSS/issues/885)
+	* Fix broken links for extension script / style files. [#862](https://github.com/FreshRSS/FreshRSS/issues/862)
+	* Load default configuration during installation to avoid hard-coded values. [#890](https://github.com/FreshRSS/FreshRSS/issues/890)
+	* Fix non-consistent behaviour in Minz_Request::getBaseUrl() and introduce Minz_Request::guessBaseUrl(). [#906](https://github.com/FreshRSS/FreshRSS/issues/906)
+	* Generate `base_url` during the installation and add a `pubsubhubbub_enabled` configuration key. [#865](https://github.com/FreshRSS/FreshRSS/issues/865)
+	* Load configuration by recursion to overwrite array values. [#923](https://github.com/FreshRSS/FreshRSS/issues/923)
+	* Cast `$limits` configuration values in integer. [#925](https://github.com/FreshRSS/FreshRSS/issues/925)
+	* Don't hide errors in configuration. [#920](https://github.com/FreshRSS/FreshRSS/issues/920)
+
+
 ## 2015-05-31 FreshRSS 1.1.1 (beta)
 ## 2015-05-31 FreshRSS 1.1.1 (beta)
 
 
 * Features
 * Features

+ 2 - 1
README.fr.md

@@ -6,6 +6,7 @@ FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed]
 Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
 Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
 
 
 Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
 Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme.
+Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des notifications instantanées depuis les sites compatibles.
 
 
 * Site officiel : http://freshrss.org
 * Site officiel : http://freshrss.org
 * Démo : http://demo.freshrss.org/
 * Démo : http://demo.freshrss.org/
@@ -32,7 +33,7 @@ Nous sommes une communauté amicale.
 * Serveur modeste, par exemple sous Linux ou Windows
 * Serveur modeste, par exemple sous Linux ou Windows
 	* Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées)
 	* Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées)
 * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
 * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
-* PHP 5.2.1+ (PHP 5.3.7+ recommandé)
+* PHP 5.2.1+ (PHP 5.3.7+ recommandé, et PHP 5.5+ pour les performances) (support bêta de PHP 7 avec encore meilleures performances)
 	* Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés)
 	* Requis : [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés)
 	* Recommandés : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
 	* Recommandés : [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
 * MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+
 * MySQL 5.0.3+ (recommandé) ou SQLite 3.7.4+

+ 2 - 1
README.md

@@ -6,6 +6,7 @@ FreshRSS is a self-hosted RSS feed aggregator such as [Leed](http://projet.idlem
 It is at the same time lightweight, easy to work with, powerful and customizable.
 It is at the same time lightweight, easy to work with, powerful and customizable.
 
 
 It is a multi-user application with an anonymous reading mode.
 It is a multi-user application with an anonymous reading mode.
+It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant notifications from compatible Web sites.
 
 
 * Official website: http://freshrss.org
 * Official website: http://freshrss.org
 * Demo: http://demo.freshrss.org/
 * Demo: http://demo.freshrss.org/
@@ -32,7 +33,7 @@ We are a friendly community.
 * Light server running Linux or Windows
 * Light server running Linux or Windows
 	* It even works on Raspberry Pi with response time under a second (tested with 150 feeds, 22k articles, or 32Mo of compressed data)
 	* It even works on Raspberry Pi with response time under a second (tested with 150 feeds, 22k articles, or 32Mo of compressed data)
 * A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
 * A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
-* PHP 5.2.1+ (PHP 5.3.7+ recommended)
+* PHP 5.2.1+ (PHP 5.3.7+ recommended, and PHP 5.5+ for performance) (beta support for PHP 7 with even higher performance)
 	* 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) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names)
 	* 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) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names)
 	* Recommended extensions: [JSON](http://php.net/json), [mbstring](http://php.net/mbstring), [zlib](http://php.net/zlib), [Zip](http://php.net/zip)
 	* Recommended 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+ (recommended) or SQLite 3.7.4+
 * MySQL 5.0.3+ (recommended) or SQLite 3.7.4+

+ 12 - 1
app/Controllers/authController.php

@@ -253,7 +253,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 				FreshRSS_Auth::giveAccess();
 				FreshRSS_Auth::giveAccess();
 				invalidateHttpCache();
 				invalidateHttpCache();
 			} else {
 			} else {
-				Minz_Log::error($reason);
+				Minz_Log::warning($reason);
 
 
 				$res = array();
 				$res = array();
 				$res['status'] = 'failure';
 				$res['status'] = 'failure';
@@ -346,4 +346,15 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 			}
 			}
 		}
 		}
 	}
 	}
+
+	/**
+	 * This action gives possibility to a user to create an account.
+	 */
+	public function registerAction() {
+		if (max_registrations_reached()) {
+			Minz_Error::error(403);
+		}
+
+		Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
+	}
 }
 }

+ 72 - 26
app/Controllers/feedController.php

@@ -98,10 +98,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 
 			// HTTP information are useful if feed is protected behind a
 			// HTTP information are useful if feed is protected behind a
 			// HTTP authentication
 			// HTTP authentication
-			$user = Minz_Request::param('http_user');
-			$pass = Minz_Request::param('http_pass');
+			$user = trim(Minz_Request::param('http_user', ''));
+			$pass = Minz_Request::param('http_pass', '');
 			$http_auth = '';
 			$http_auth = '';
-			if ($user != '' || $pass != '') {
+			if ($user != '' && $pass != '') {	//TODO: Sanitize
 				$http_auth = $user . ':' . $pass;
 				$http_auth = $user . ':' . $pass;
 			}
 			}
 
 
@@ -168,6 +168,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			// Ok, feed has been added in database. Now we have to refresh entries.
 			// Ok, feed has been added in database. Now we have to refresh entries.
 			$feed->_id($id);
 			$feed->_id($id);
 			$feed->faviconPrepare();
 			$feed->faviconPrepare();
+			//$feed->pubSubHubbubPrepare();	//TODO: prepare PubSubHubbub already when adding the feed
 
 
 			$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 			$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 
 
@@ -261,12 +262,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 	 * This action actualizes entries from one or several feeds.
 	 * This action actualizes entries from one or several feeds.
 	 *
 	 *
 	 * Parameters are:
 	 * Parameters are:
-	 *   - id (default: false)
+	 *   - id (default: false): Feed ID
+	 *   - url (default: false): Feed URL
 	 *   - force (default: false)
 	 *   - force (default: false)
-	 * If id is not specified, all the feeds are actualized. But if force is
+	 * If id and url are not specified, all the feeds are actualized. But if force is
 	 * false, process stops at 10 feeds to avoid time execution problem.
 	 * false, process stops at 10 feeds to avoid time execution problem.
 	 */
 	 */
-	public function actualizeAction() {
+	public function actualizeAction($simplePiePush = null) {
 		@set_time_limit(300);
 		@set_time_limit(300);
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -274,14 +276,15 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 
 		Minz_Session::_param('actualize_feeds', false);
 		Minz_Session::_param('actualize_feeds', false);
 		$id = Minz_Request::param('id');
 		$id = Minz_Request::param('id');
+		$url = Minz_Request::param('url');
 		$force = Minz_Request::param('force');
 		$force = Minz_Request::param('force');
 
 
 		// Create a list of feeds to actualize.
 		// Create a list of feeds to actualize.
 		// If id is set and valid, corresponding feed is added to the list but
 		// If id is set and valid, corresponding feed is added to the list but
 		// alone in order to automatize further process.
 		// alone in order to automatize further process.
 		$feeds = array();
 		$feeds = array();
-		if ($id) {
-			$feed = $feedDAO->searchById($id);
+		if ($id || $url) {
+			$feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url);
 			if ($feed) {
 			if ($feed) {
 				$feeds[] = $feed;
 				$feeds[] = $feed;
 			}
 			}
@@ -293,20 +296,36 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
 		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
 		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
 		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
 
 
+		// PubSubHubbub support
+		$pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
+		$pshbMinAge = time() - (3600 * 24);  //TODO: Make a configuration.
+
 		$updated_feeds = 0;
 		$updated_feeds = 0;
 		$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 		$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 		foreach ($feeds as $feed) {
 		foreach ($feeds as $feed) {
+			$url = $feed->url();	//For detection of HTTP 301
+
+			$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
+			if ((!$simplePiePush) && (!$id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
+				$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
+				//Minz_Log::debug($text);
+				file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+				continue;	//When PubSubHubbub is used, do not pull refresh so often
+			}
+
 			if (!$feed->lock()) {
 			if (!$feed->lock()) {
 				Minz_Log::notice('Feed already being actualized: ' . $feed->url());
 				Minz_Log::notice('Feed already being actualized: ' . $feed->url());
 				continue;
 				continue;
 			}
 			}
 
 
-			$url = $feed->url();	//For detection of HTTP 301
 			try {
 			try {
-				// Load entries
-				$feed->load(false);
+				if ($simplePiePush) {
+					$feed->loadEntries($simplePiePush);	//Used by PubSubHubbub
+				} else {
+					$feed->load(false);
+				}
 			} catch (FreshRSS_Feed_Exception $e) {
 			} catch (FreshRSS_Feed_Exception $e) {
-				Minz_Log::notice($e->getMessage());
+				Minz_Log::warning($e->getMessage());
 				$feedDAO->updateLastUpdate($feed->id(), true);
 				$feedDAO->updateLastUpdate($feed->id(), true);
 				$feed->unlock();
 				$feed->unlock();
 				continue;
 				continue;
@@ -368,6 +387,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 							continue;
 							continue;
 						}
 						}
 
 
+						if ($pubSubHubbubEnabled && !$simplePiePush) {	//We use push, but have discovered an article by pull!
+							$text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . ' GUID ' . $entry->guid();
+							file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+							Minz_Log::warning($text);
+							$pubSubHubbubEnabled = false;
+							$feed->pubSubHubbubError(true);
+						}
+
 						if (!$entryDAO->hasTransaction()) {
 						if (!$entryDAO->hasTransaction()) {
 							$entryDAO->beginTransaction();
 							$entryDAO->beginTransaction();
 						}
 						}
@@ -398,13 +425,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$entryDAO->commit();
 				$entryDAO->commit();
 			}
 			}
 
 
-			if ($feed->url() !== $url) {
-				// HTTP 301 Moved Permanently
+			if ($feed->hubUrl() && $feed->selfUrl()) {	//selfUrl has priority for PubSubHubbub
+				if ($feed->selfUrl() !== $url) {	//https://code.google.com/p/pubsubhubbub/wiki/MovingFeedsOrChangingHubs
+					$selfUrl = checkUrl($feed->selfUrl());
+					if ($selfUrl) {
+						Minz_Log::debug('PubSubHubbub unsubscribe ' . $feed->url());
+						if (!$feed->pubSubHubbubSubscribe(false)) {	//Unsubscribe
+							Minz_Log::warning('Error while PubSubHubbub unsubscribing from ' . $feed->url());
+						}
+						$feed->_url($selfUrl, false);
+						Minz_Log::notice('Feed ' . $url . ' canonical address moved to ' . $feed->url());
+						$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
+					}
+				}
+			}
+			elseif ($feed->url() !== $url) {	// HTTP 301 Moved Permanently
 				Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url());
 				Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url());
 				$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
 				$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
 			}
 			}
 
 
 			$feed->faviconPrepare();
 			$feed->faviconPrepare();
+			if ($pubsubhubbubEnabledGeneral && $feed->pubSubHubbubPrepare()) {
+				Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url());
+				if (!$feed->pubSubHubbubSubscribe(true)) {	//Subscribe
+					Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url());
+				}
+			}
 			$feed->unlock();
 			$feed->unlock();
 			$updated_feeds++;
 			$updated_feeds++;
 			unset($feed);
 			unset($feed);
@@ -427,20 +473,20 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			Minz_Session::_param('notification', $notif);
 			Minz_Session::_param('notification', $notif);
 			// No layout in ajax request.
 			// No layout in ajax request.
 			$this->view->_useLayout(false);
 			$this->view->_useLayout(false);
-			return;
-		}
-
-		// Redirect to the main page with correct notification.
-		if ($updated_feeds === 1) {
-			$feed = reset($feeds);
-			Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array(
-				'params' => array('get' => 'f_' . $feed->id())
-			));
-		} elseif ($updated_feeds > 1) {
-			Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array());
 		} else {
 		} else {
-			Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array());
+			// Redirect to the main page with correct notification.
+			if ($updated_feeds === 1) {
+				$feed = reset($feeds);
+				Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array(
+					'params' => array('get' => 'f_' . $feed->id())
+				));
+			} elseif ($updated_feeds > 1) {
+				Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array());
+			} else {
+				Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array());
+			}
 		}
 		}
+		return $updated_feeds;
 	}
 	}
 
 
 	/**
 	/**

+ 3 - 3
app/Controllers/importExportController.php

@@ -47,7 +47,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$status_file = $file['error'];
 		$status_file = $file['error'];
 
 
 		if ($status_file !== 0) {
 		if ($status_file !== 0) {
-			Minz_Log::error('File cannot be uploaded. Error code: ' . $status_file);
+			Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
 			Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
 			Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
 			                  array('c' => 'importExport', 'a' => 'index'));
 			                  array('c' => 'importExport', 'a' => 'index'));
 		}
 		}
@@ -69,7 +69,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 
 			if (!is_resource($zip)) {
 			if (!is_resource($zip)) {
 				// zip_open cannot open file: something is wrong
 				// zip_open cannot open file: something is wrong
-				Minz_Log::error('Zip archive cannot be imported. Error code: ' . $zip);
+				Minz_Log::warning('Zip archive cannot be imported. Error code: ' . $zip);
 				Minz_Request::bad(_t('feedback.import_export.zip_error'),
 				Minz_Request::bad(_t('feedback.import_export.zip_error'),
 				                  array('c' => 'importExport', 'a' => 'index'));
 				                  array('c' => 'importExport', 'a' => 'index'));
 			}
 			}
@@ -77,7 +77,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			while (($zipfile = zip_read($zip)) !== false) {
 			while (($zipfile = zip_read($zip)) !== false) {
 				if (!is_resource($zipfile)) {
 				if (!is_resource($zipfile)) {
 					// zip_entry() can also return an error code!
 					// zip_entry() can also return an error code!
-					Minz_Log::error('Zip file cannot be imported. Error code: ' . $zipfile);
+					Minz_Log::warning('Zip file cannot be imported. Error code: ' . $zipfile);
 				} else {
 				} else {
 					$type_zipfile = $this->guessFileType(zip_entry_name($zipfile));
 					$type_zipfile = $this->guessFileType(zip_entry_name($zipfile));
 					if ($type_file !== 'unknown') {
 					if ($type_file !== 'unknown') {

+ 3 - 3
app/Controllers/subscriptionController.php

@@ -77,11 +77,11 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 		Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · ');
 		Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · ');
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
-			$user = Minz_Request::param('http_user', '');
-			$pass = Minz_Request::param('http_pass', '');
+			$user = trim(Minz_Request::param('http_user_feed' . $id, ''));
+			$pass = Minz_Request::param('http_pass_feed' . $id, '');
 
 
 			$httpAuth = '';
 			$httpAuth = '';
-			if ($user != '' || $pass != '') {
+			if ($user != '' && $pass != '') {	//TODO: Sanitize
 				$httpAuth = $user . ':' . $pass;
 				$httpAuth = $user . ':' . $pass;
 			}
 			}
 
 

+ 1 - 1
app/Controllers/updateController.php

@@ -63,7 +63,7 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 		curl_close($c);
 		curl_close($c);
 
 
 		if ($c_status !== 200) {
 		if ($c_status !== 200) {
-			Minz_Log::error(
+			Minz_Log::warning(
 				'Error during update (HTTP code ' . $c_status . '): ' . $c_error
 				'Error during update (HTTP code ' . $c_status . '): ' . $c_error
 			);
 			);
 
 

+ 93 - 8
app/Controllers/userController.php

@@ -12,9 +12,14 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * This action is called before every other action in that class. It is
 	 * This action is called before every other action in that class. It is
 	 * the common boiler plate for every action. It is triggered by the
 	 * the common boiler plate for every action. It is triggered by the
 	 * underlying framework.
 	 * underlying framework.
+	 *
+	 * @todo clean up the access condition.
 	 */
 	 */
 	public function firstAction() {
 	public function firstAction() {
-		if (!FreshRSS_Auth::hasAccess()) {
+		if (!FreshRSS_Auth::hasAccess() && !(
+				Minz_Request::actionName() === 'create' &&
+				!max_registrations_reached()
+		)) {
 			Minz_Error::error(403);
 			Minz_Error::error(403);
 		}
 		}
 	}
 	}
@@ -25,13 +30,17 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	public function profileAction() {
 	public function profileAction() {
 		Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
 		Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
 
 
+		Minz_View::appendScript(Minz_Url::display(
+			'/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
+		));
+
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
 			$ok = true;
 			$ok = true;
 
 
-			$passwordPlain = Minz_Request::param('passwordPlain', '', true);
+			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			if ($passwordPlain != '') {
 			if ($passwordPlain != '') {
-				Minz_Request::_param('passwordPlain');	//Discard plain-text password ASAP
-				$_POST['passwordPlain'] = '';
+				Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
+				$_POST['newPasswordPlain'] = '';
 				if (!function_exists('password_hash')) {
 				if (!function_exists('password_hash')) {
 					include_once(LIB_PATH . '/password_compat.php');
 					include_once(LIB_PATH . '/password_compat.php');
 				}
 				}
@@ -103,8 +112,24 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		$this->view->size_user = $entryDAO->size();
 		$this->view->size_user = $entryDAO->size();
 	}
 	}
 
 
+	/**
+	 * This action creates a new user.
+	 *
+	 * Request parameters are:
+	 *   - new_user_language
+	 *   - new_user_name
+	 *   - new_user_passwordPlain
+	 *   - new_user_email
+	 *   - r (i.e. a redirection url, optional)
+	 *
+	 * @todo clean up this method. Idea: write a method to init a user with basic information.
+	 * @todo handle r redirection in Minz_Request::forward directly?
+	 */
 	public function createAction() {
 	public function createAction() {
-		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
+		if (Minz_Request::isPost() && (
+				FreshRSS_Auth::hasAccess('admin') ||
+				!max_registrations_reached()
+		)) {
 			$db = FreshRSS_Context::$system_conf->db;
 			$db = FreshRSS_Context::$system_conf->db;
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
 
@@ -175,15 +200,37 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			Minz_Session::_param('notification', $notif);
 			Minz_Session::_param('notification', $notif);
 		}
 		}
 
 
-		Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true);
+		$redirect_url = urldecode(Minz_Request::param('r', false, true));
+		if (!$redirect_url) {
+			$redirect_url = array('c' => 'user', 'a' => 'manage');
+		}
+		Minz_Request::forward($redirect_url, true);
 	}
 	}
 
 
+	/**
+	 * This action delete an existing user.
+	 *
+	 * Request parameter is:
+	 *   - username
+	 *
+	 * @todo clean up this method. Idea: create a User->clean() method.
+	 */
 	public function deleteAction() {
 	public function deleteAction() {
-		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
+		$username = Minz_Request::param('username');
+		$redirect_url = urldecode(Minz_Request::param('r', false, true));
+		if (!$redirect_url) {
+			$redirect_url = array('c' => 'user', 'a' => 'manage');
+		}
+
+		$self_deletion = Minz_Session::param('currentUser', '_') === $username;
+
+		if (Minz_Request::isPost() && (
+				FreshRSS_Auth::hasAccess('admin') ||
+				$self_deletion
+		)) {
 			$db = FreshRSS_Context::$system_conf->db;
 			$db = FreshRSS_Context::$system_conf->db;
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
 
-			$username = Minz_Request::param('username');
 			$ok = ctype_alnum($username);
 			$ok = ctype_alnum($username);
 			$user_data = join_path(DATA_PATH, 'users', $username);
 			$user_data = join_path(DATA_PATH, 'users', $username);
 
 
@@ -191,6 +238,16 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				$default_user = FreshRSS_Context::$system_conf->default_user;
 				$default_user = FreshRSS_Context::$system_conf->default_user;
 				$ok &= (strcasecmp($username, $default_user) !== 0);	//It is forbidden to delete the default user
 				$ok &= (strcasecmp($username, $default_user) !== 0);	//It is forbidden to delete the default user
 			}
 			}
+			if ($ok && $self_deletion) {
+				// We check the password if it's a self-destruction
+				$nonce = Minz_Session::param('nonce');
+				$challenge = Minz_Request::param('challenge', '');
+
+				$ok &= FreshRSS_FormAuth::checkCredentials(
+					$username, FreshRSS_Context::$user_conf->passwordHash,
+					$nonce, $challenge
+				);
+			}
 			if ($ok) {
 			if ($ok) {
 				$ok &= is_dir($user_data);
 				$ok &= is_dir($user_data);
 			}
 			}
@@ -200,6 +257,10 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				$ok &= recursive_unlink($user_data);
 				$ok &= recursive_unlink($user_data);
 				//TODO: delete Persona file
 				//TODO: delete Persona file
 			}
 			}
+			if ($ok && $self_deletion) {
+				FreshRSS_Auth::removeAccess();
+				$redirect_url = array('c' => 'index', 'a' => 'index');
+			}
 			invalidateHttpCache();
 			invalidateHttpCache();
 
 
 			$notif = array(
 			$notif = array(
@@ -209,6 +270,30 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			Minz_Session::_param('notification', $notif);
 			Minz_Session::_param('notification', $notif);
 		}
 		}
 
 
+		Minz_Request::forward($redirect_url, true);
+	}
+
+	/**
+	 * This action updates the max number of registrations.
+	 *
+	 * Request parameter is:
+	 *   - max-registrations (int >= 0)
+	 */
+	public function setRegistrationAction() {
+		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
+			$limits = FreshRSS_Context::$system_conf->limits;
+			$limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
+			FreshRSS_Context::$system_conf->limits = $limits;
+			FreshRSS_Context::$system_conf->save();
+
+			invalidateHttpCache();
+
+			Minz_Session::_param('notification', array(
+				'type' => 'good',
+				'content' => _t('feedback.user.set_registration')
+			));
+		}
+
 		Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true);
 		Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true);
 	}
 	}
 }
 }

+ 1 - 1
app/Models/CategoryDAO.php

@@ -13,7 +13,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			return $this->bd->lastInsertId();
 			return $this->bd->lastInsertId();
 		} else {
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error addCategory: ' . $info[2]	);
+			Minz_Log::error('SQL error addCategory: ' . $info[2]);
 			return false;
 			return false;
 		}
 		}
 	}
 	}

+ 6 - 2
app/Models/ConfigurationSetter.php

@@ -352,6 +352,9 @@ class FreshRSS_ConfigurationSetter {
 				'min' => 0,
 				'min' => 0,
 				'max' => $max_small_int,
 				'max' => $max_small_int,
 			),
 			),
+			'max_registrations' => array(
+				'min' => 0,
+			),
 		);
 		);
 
 
 		foreach ($values as $key => $value) {
 		foreach ($values as $key => $value) {
@@ -359,10 +362,11 @@ class FreshRSS_ConfigurationSetter {
 				continue;
 				continue;
 			}
 			}
 
 
+			$value = intval($value);
 			$limits = $limits_keys[$key];
 			$limits = $limits_keys[$key];
 			if (
 			if (
-				(!isset($limits['min']) || $value > $limits['min']) &&
-				(!isset($limits['max']) || $value < $limits['max'])
+				(!isset($limits['min']) || $value >= $limits['min']) &&
+				(!isset($limits['max']) || $value <= $limits['max'])
 			) {
 			) {
 				$data['limits'][$key] = $value;
 				$data['limits'][$key] = $value;
 			}
 			}

+ 12 - 5
app/Models/EntryDAO.php

@@ -6,6 +6,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return parent::$sharedDbType !== 'sqlite';
 		return parent::$sharedDbType !== 'sqlite';
 	}
 	}
 
 
+	public function hasNativeHex() {
+		return parent::$sharedDbType !== 'sqlite';
+	}
+
 	protected function addColumn($name) {
 	protected function addColumn($name) {
 		Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn: ' . $name);
 		Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn: ' . $name);
 		$hasTransaction = false;
 		$hasTransaction = false;
@@ -64,7 +68,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			     . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) '
 			     . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) '
 			     . 'VALUES(?, ?, ?, ?, '
 			     . 'VALUES(?, ?, ?, ?, '
 			     . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
 			     . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
-			     . ', ?, ?, ?, ?, ?, ?, ?, ?)';
+			     . ', ?, ?, ?, '
+			     . ($this->hasNativeHex() ? 'X?' : '?')
+			     . ', ?, ?, ?, ?)';
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 		}
 		}
 
 
@@ -77,7 +83,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			substr($valuesTmp['link'], 0, 1023),
 			substr($valuesTmp['link'], 0, 1023),
 			$valuesTmp['date'],
 			$valuesTmp['date'],
 			time(),
 			time(),
-			hex2bin($valuesTmp['hash']),	// X'09AF' hexadecimal literals do not work with SQLite/PDO
+			$this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),	// X'09AF' hexadecimal literals do not work with SQLite/PDO	//hex2bin() is PHP5.4+
 			$valuesTmp['is_read'] ? 1 : 0,
 			$valuesTmp['is_read'] ? 1 : 0,
 			$valuesTmp['is_favorite'] ? 1 : 0,
 			$valuesTmp['is_favorite'] ? 1 : 0,
 			$valuesTmp['id_feed'],
 			$valuesTmp['id_feed'],
@@ -109,8 +115,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$sql = 'UPDATE `' . $this->prefix . 'entry` '
 			$sql = 'UPDATE `' . $this->prefix . 'entry` '
 			     . 'SET title=?, author=?, '
 			     . 'SET title=?, author=?, '
 			     . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?')
 			     . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?')
-			     . ', link=?, date=?, lastSeen=?, hash=?, '
-			     . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ')
+			     . ', link=?, date=?, lastSeen=?, hash='
+			     . ($this->hasNativeHex() ? 'X?' : '?')
+			     . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ')
 			     . 'tags=? '
 			     . 'tags=? '
 			     . 'WHERE id_feed=? AND guid=?';
 			     . 'WHERE id_feed=? AND guid=?';
 			$this->updateEntryPrepared = $this->bd->prepare($sql);
 			$this->updateEntryPrepared = $this->bd->prepare($sql);
@@ -123,7 +130,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			substr($valuesTmp['link'], 0, 1023),
 			substr($valuesTmp['link'], 0, 1023),
 			$valuesTmp['date'],
 			$valuesTmp['date'],
 			time(),
 			time(),
-			hex2bin($valuesTmp['hash']),
+			$this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),
 		);
 		);
 		if ($valuesTmp['is_read'] !== null) {
 		if ($valuesTmp['is_read'] !== null) {
 			$values[] = $valuesTmp['is_read'] ? 1 : 0;
 			$values[] = $valuesTmp['is_read'] ? 1 : 0;

+ 149 - 1
app/Models/Feed.php

@@ -19,6 +19,8 @@ class FreshRSS_Feed extends Minz_Model {
 	private $ttl = -2;
 	private $ttl = -2;
 	private $hash = null;
 	private $hash = null;
 	private $lockPath = '';
 	private $lockPath = '';
+	private $hubUrl = '';
+	private $selfUrl = '';
 
 
 	public function __construct($url, $validate=true) {
 	public function __construct($url, $validate=true) {
 		if ($validate) {
 		if ($validate) {
@@ -49,6 +51,12 @@ class FreshRSS_Feed extends Minz_Model {
 	public function url() {
 	public function url() {
 		return $this->url;
 		return $this->url;
 	}
 	}
+	public function selfUrl() {
+		return $this->selfUrl;
+	}
+	public function hubUrl() {
+		return $this->hubUrl;
+	}
 	public function category() {
 	public function category() {
 		return $this->category;
 		return $this->category;
 	}
 	}
@@ -96,6 +104,16 @@ class FreshRSS_Feed extends Minz_Model {
 	public function ttl() {
 	public function ttl() {
 		return $this->ttl;
 		return $this->ttl;
 	}
 	}
+	// public function ttlExpire() {
+		// $ttl = $this->ttl;
+		// if ($ttl == -2) {	//Default
+			// $ttl = FreshRSS_Context::$user_conf->ttl_default;
+		// }
+		// if ($ttl == -1) {	//Never
+			// $ttl = 64000000;	//~2 years. Good enough for PubSubHubbub logic
+		// }
+		// return $this->lastUpdate + $ttl;
+	// }
 	public function nbEntries() {
 	public function nbEntries() {
 		if ($this->nbEntries < 0) {
 		if ($this->nbEntries < 0) {
 			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -226,6 +244,11 @@ class FreshRSS_Feed extends Minz_Model {
 					throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Feed error' : $errorMessage) . ' [' . $url . ']');
 					throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Feed error' : $errorMessage) . ' [' . $url . ']');
 				}
 				}
 
 
+				$links = $feed->get_links('self');
+				$this->selfUrl = isset($links[0]) ? $links[0] : null;
+				$links = $feed->get_links('hub');
+				$this->hubUrl = isset($links[0]) ? $links[0] : null;
+
 				if ($loadDetails) {
 				if ($loadDetails) {
 					// si on a utilisé l'auto-discover, notre url va avoir changé
 					// si on a utilisé l'auto-discover, notre url va avoir changé
 					$subscribe_url = $feed->subscribe_url(false);
 					$subscribe_url = $feed->subscribe_url(false);
@@ -259,7 +282,7 @@ class FreshRSS_Feed extends Minz_Model {
 		}
 		}
 	}
 	}
 
 
-	private function loadEntries($feed) {
+	public function loadEntries($feed) {
 		$entries = array();
 		$entries = array();
 
 
 		foreach ($feed->get_items() as $item) {
 		foreach ($feed->get_items() as $item) {
@@ -333,4 +356,129 @@ class FreshRSS_Feed extends Minz_Model {
 	function unlock() {
 	function unlock() {
 		@unlink($this->lockPath);
 		@unlink($this->lockPath);
 	}
 	}
+
+	//<PubSubHubbub>
+
+	function pubSubHubbubEnabled() {
+		$url = $this->selfUrl ? $this->selfUrl : $this->url;
+		$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
+		if ($hubFile = @file_get_contents($hubFilename)) {
+			$hubJson = json_decode($hubFile, true);
+			if ($hubJson && empty($hubJson['error']) &&
+				(empty($hubJson['lease_end']) || $hubJson['lease_end'] > time())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	function pubSubHubbubError($error = true) {
+		$url = $this->selfUrl ? $this->selfUrl : $this->url;
+		$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
+		$hubFile = @file_get_contents($hubFilename);
+		$hubJson = $hubFile ? json_decode($hubFile, true) : array();
+		if (!isset($hubJson['error']) || $hubJson['error'] !== (bool)$error) {
+			$hubJson['error'] = (bool)$error;
+			file_put_contents($hubFilename, json_encode($hubJson));
+			file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t"
+				. 'Set error to ' . ($error ? 1 : 0) . ' for ' . $url . "\n", FILE_APPEND);
+		}
+		return false;
+	}
+
+	function pubSubHubbubPrepare() {
+		$key = '';
+		if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) {
+			$path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl);
+			$hubFilename = $path . '/!hub.json';
+			if ($hubFile = @file_get_contents($hubFilename)) {
+				$hubJson = json_decode($hubFile, true);
+				if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
+					$text = 'Invalid JSON for PubSubHubbub: ' . $this->url;
+					Minz_Log::warning($text);
+					file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+					return false;
+				}
+				if ((!empty($hubJson['lease_end'])) && ($hubJson['lease_end'] < (time() + (3600 * 23)))) {	//TODO: Make a better policy
+					$text = 'PubSubHubbub lease ends at '
+						. date('c', empty($hubJson['lease_end']) ? time() : $hubJson['lease_end'])
+						. ' and needs renewal: ' . $this->url;
+					Minz_Log::warning($text);
+					file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+					$key = $hubJson['key'];	//To renew our lease
+				} elseif (((!empty($hubJson['error'])) || empty($hubJson['lease_end'])) &&
+					(empty($hubJson['lease_start']) || $hubJson['lease_start'] < time() - (3600 * 23))) {	//Do not renew too often
+					$key = $hubJson['key'];	//To renew our lease
+				}
+			} else {
+				@mkdir($path, 0777, true);
+				$key = sha1($path . FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true));
+				$hubJson = array(
+					'hub' => $this->hubUrl,
+					'key' => $key,
+				);
+				file_put_contents($hubFilename, json_encode($hubJson));
+				@mkdir(PSHB_PATH . '/keys/');
+				file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', base64url_encode($this->selfUrl));
+				$text = 'PubSubHubbub prepared for ' . $this->url;
+				Minz_Log::debug($text);
+				file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+			}
+			$currentUser = Minz_Session::param('currentUser');
+			if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) {
+				touch($path . '/' . $currentUser . '.txt');
+			}
+		}
+		return $key;
+	}
+
+	//Parameter true to subscribe, false to unsubscribe.
+	function pubSubHubbubSubscribe($state) {
+		if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) {
+			$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json';
+			$hubFile = @file_get_contents($hubFilename);
+			if ($hubFile === false) {
+				Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url);
+				return false;
+			}
+			$hubJson = json_decode($hubFile, true);
+			if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
+				Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url);
+				return false;
+			}
+			$callbackUrl = checkUrl(FreshRSS_Context::$system_conf->base_url . 'api/pshb.php?k=' . $hubJson['key']);
+			if ($callbackUrl == '') {
+				Minz_Log::warning('Invalid callback for PubSubHubbub: ' . $this->url);
+				return false;
+			}
+			$ch = curl_init();
+			curl_setopt_array($ch, array(
+				CURLOPT_URL => $this->hubUrl,
+				CURLOPT_FOLLOWLOCATION => true,
+				CURLOPT_RETURNTRANSFER => true,
+				CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
+				CURLOPT_POSTFIELDS => 'hub.verify=sync'
+					. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
+					. '&hub.topic=' . urlencode($this->selfUrl)
+					. '&hub.callback=' . urlencode($callbackUrl)
+				)
+			);
+			$response = curl_exec($ch);
+			$info = curl_getinfo($ch);
+
+			file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" .
+				'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl .
+				' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND);
+
+			if (!$state) {	//unsubscribe
+				$hubJson['lease_end'] = time() - 60;
+				file_put_contents($hubFilename, json_encode($hubJson));
+			}
+
+			return substr($info['http_code'], 0, 1) == '2';
+		}
+		return false;
+	}
+
+	//</PubSubHubbub>
 }
 }

+ 2 - 0
app/SQL/install.sql.mysql.php

@@ -57,6 +57,8 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` (
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
 INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
 INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
+INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
+INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);
 ');
 ');
 
 
 define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
 define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');

+ 2 - 0
app/SQL/install.sql.sqlite.php

@@ -55,6 +55,8 @@ $SQL_CREATE_TABLES = array(
 'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);',	//v1.1.1
 'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);',	//v1.1.1
 
 
 'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");',
 'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");',
+'INSERT OR IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);',
+'INSERT OR IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);',
 );
 );
 
 
 define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');
 define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory');

+ 7 - 0
app/i18n/cz/admin.php

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Vytvořit nového uživatele',
 		'create' => 'Vytvořit nového uživatele',
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Jazyk',
 		'language' => 'Jazyk',
+		'number' => 'Zatím je vytvořen %d účet',
+		'numbers' => 'Zatím je vytvořeno %d účtů',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
 		'password_format' => 'Alespoň 7 znaků',
 		'password_format' => 'Alespoň 7 znaků',
+		'registration' => array(
+			'allow' => 'Povolit vytváření účtů',
+			'help' => '0 znamená žádná omezení účtu',
+			'number' => 'Maximální počet účtů',
+		),
 		'title' => 'Správa uživatelů',
 		'title' => 'Správa uživatelů',
 		'user_list' => 'Seznam uživatelů',
 		'user_list' => 'Seznam uživatelů',
 		'username' => 'Přihlašovací jméno',
 		'username' => 'Přihlašovací jméno',

+ 5 - 0
app/i18n/cz/conf.php

@@ -72,6 +72,10 @@ return array(
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'_' => 'Správa profilu',
 		'_' => 'Správa profilu',
+		'delete' => array(
+			'_' => 'Smazání účtu',
+			'warn' => 'Váš účet bude smazán spolu se všemi souvisejícími daty',
+		),
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'password_api' => 'Password API<br /><small>(tzn. pro mobilní aplikace)</small>',
 		'password_api' => 'Password API<br /><small>(tzn. pro mobilní aplikace)</small>',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
@@ -84,6 +88,7 @@ return array(
 		'articles_per_page' => 'Počet článků na stranu',
 		'articles_per_page' => 'Počet článků na stranu',
 		'auto_load_more' => 'Načítat další články dole na stránce',
 		'auto_load_more' => 'Načítat další články dole na stránce',
 		'auto_remove_article' => 'Po přečtení články schovat',
 		'auto_remove_article' => 'Po přečtení články schovat',
+		'mark_updated_article_unread' => 'Označte aktualizované položky jako nepřečtené',
 		'confirm_enabled' => 'Vyžadovat potvrzení pro akci “označit vše jako přečtené”',
 		'confirm_enabled' => 'Vyžadovat potvrzení pro akci “označit vše jako přečtené”',
 		'display_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené',
 		'display_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené',
 		'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené',
 		'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené',

+ 1 - 0
app/i18n/cz/feedback.php

@@ -102,6 +102,7 @@ return array(
 			'_' => 'Uživatel %s byl smazán',
 			'_' => 'Uživatel %s byl smazán',
 			'error' => 'Uživatele %s nelze smazat',
 			'error' => 'Uživatele %s nelze smazat',
 		),
 		),
+		'set_registration' => 'Maximální počet účtů byl změněn',
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'error' => 'Váš profil nelze změnit',
 		'error' => 'Váš profil nelze změnit',

+ 15 - 3
app/i18n/cz/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Smazat všechny články',
 		'truncate' => 'Smazat všechny články',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'email' => 'Email',
 		'keep_logged_in' => 'Zapamatovat přihlášení <small>(1 měsíc)</small>',
 		'keep_logged_in' => 'Zapamatovat přihlášení <small>(1 měsíc)</small>',
 		'login' => 'Login',
 		'login' => 'Login',
 		'login_persona' => 'Přihlášení pomocí Persona',
 		'login_persona' => 'Přihlášení pomocí Persona',
 		'login_persona_problem' => 'Problém s připojením k Persona?',
 		'login_persona_problem' => 'Problém s připojením k Persona?',
 		'logout' => 'Odhlášení',
 		'logout' => 'Odhlášení',
-		'password' => 'Heslo',
+		'password' =>  array(
+			'_' => 'Heslo',
+			'format' => '<small>Alespoň 7 znaků</small>',
+		),
+		'registration' => array(
+			'_' => 'Nový účet',
+			'ask' => 'Vytvořit účet?',
+			'title' => 'Vytvoření účtu',
+		),
 		'reset' => 'Reset přihlášení',
 		'reset' => 'Reset přihlášení',
-		'username' => 'Uživatel',
-		'username_admin' => 'Název administrátorského účtu',
+		'username' => array(
+			'_' => 'Uživatel',
+			'admin' => 'Název administrátorského účtu',
+			'format' => '<small>maximálně 16 alfanumerických znaků</small>',
+		),
 		'will_reset' => 'Přihlašovací systém bude vyresetován: místo sytému Persona bude použito přihlášení formulářem.',
 		'will_reset' => 'Přihlašovací systém bude vyresetován: místo sytému Persona bude použito přihlášení formulářem.',
 	),
 	),
 	'date' => array(
 	'date' => array(

+ 6 - 0
app/i18n/cz/install.php

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 	'action' => array(
 		'finish' => 'Dokončit instalaci',
 		'finish' => 'Dokončit instalaci',
 		'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.',
 		'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.',
+		'keep_install' => 'Zachovat předchozí instalaci',
 		'next_step' => 'Přejít na další krok',
 		'next_step' => 'Přejít na další krok',
+		'reinstall' => 'Reinstalovat FreshRSS',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
@@ -31,6 +33,7 @@ return array(
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Kontrola',
 		'_' => 'Kontrola',
+		'already_installed' => 'Zjistili jsme, že FreshRSS je již nainstalován!',
 		'cache' => array(
 		'cache' => array(
 			'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/cache</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
 			'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/cache</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
 			'ok' => 'Oprávnění adresáře cache jsou v pořádku.',
 			'ok' => 'Oprávnění adresáře cache jsou v pořádku.',
@@ -93,6 +96,9 @@ return array(
 	'delete_articles_after' => 'Smazat články starší než',
 	'delete_articles_after' => 'Smazat články starší než',
 	'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.',
 	'fix_errors_before' => 'Chyby prosím před přechodem na další krok opravte.',
 	'javascript_is_better' => 'Práce s FreshRSS je příjemnější se zapnutým JavaScriptem',
 	'javascript_is_better' => 'Práce s FreshRSS je příjemnější se zapnutým JavaScriptem',
+	'js' => array(
+		'confirm_reinstall' => 'Reinstalací FreshRSS ztratíte předchozí konfiguraci. Opravdu chcete pokračovat?',
+	),
 	'language' => array(
 	'language' => array(
 		'_' => 'Jazyk',
 		'_' => 'Jazyk',
 		'choose' => 'Vyberte jazyk FreshRSS',
 		'choose' => 'Vyberte jazyk FreshRSS',

+ 1 - 0
app/i18n/cz/sub.php

@@ -37,6 +37,7 @@ return array(
 		'url' => 'URL kanálu',
 		'url' => 'URL kanálu',
 		'validator' => 'Zkontrolovat platnost kanálu',
 		'validator' => 'Zkontrolovat platnost kanálu',
 		'website' => 'URL webové stránky',
 		'website' => 'URL webové stránky',
+		'pubsubhubbub' => 'Okamžité oznámení s PubSubHubbub',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
 		'export' => 'Export',
 		'export' => 'Export',

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

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Neuen Benutzer erstellen',
 		'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>',
 		'email_persona' => 'Anmelde-E-Mail-Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Sprache',
 		'language' => 'Sprache',
+		'number' => 'Es wurde bis jetzt %d Account erstellt',
+		'numbers' => 'Es wurden bis jetzt %d Accounts erstellt',
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
 		'password_format' => 'mindestens 7 Zeichen',
 		'password_format' => 'mindestens 7 Zeichen',
+		'registration' => array(
+			'allow' => 'Erlaube die Accounterstellung',
+			'help' => '0 meint, dass es kein Account Limit gibt',
+			'number' => 'Maximale Anzahl von Accounts',
+		),
 		'title' => 'Benutzer verwalten',
 		'title' => 'Benutzer verwalten',
 		'user_list' => 'Liste der Benutzer',
 		'user_list' => 'Liste der Benutzer',
 		'username' => 'Nutzername',
 		'username' => 'Nutzername',

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

@@ -72,6 +72,10 @@ return array(
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'_' => 'Profil-Verwaltung',
 		'_' => 'Profil-Verwaltung',
+		'delete' => array(
+			'_' => 'Accountlöschung',
+			'warn' => 'Dein Account und alle damit bezogenen Daten werden gelöscht.',
+		),
 		'email_persona' => 'Anmelde-E-Mail-Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'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_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_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',

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

@@ -102,6 +102,7 @@ return array(
 			'_' => 'Der Benutzer %s ist gelöscht worden',
 			'_' => 'Der Benutzer %s ist gelöscht worden',
 			'error' => 'Der Benutzer %s kann nicht gelöscht werden',
 			'error' => 'Der Benutzer %s kann nicht gelöscht werden',
 		),
 		),
+		'set_registration' => 'Die maximale Anzahl von Accounts wurde aktualisiert.',
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'error' => 'Ihr Profil kann nicht geändert werden',
 		'error' => 'Ihr Profil kann nicht geändert werden',

+ 15 - 3
app/i18n/de/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Alle Artikel löschen',
 		'truncate' => 'Alle Artikel löschen',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'email' => 'E-Mail-Adresse',
 		'keep_logged_in' => 'Eingeloggt bleiben <small>(1 Monat)</small>',
 		'keep_logged_in' => 'Eingeloggt bleiben <small>(1 Monat)</small>',
 		'login' => 'Anmelden',
 		'login' => 'Anmelden',
 		'login_persona' => 'Anmelden mit Persona',
 		'login_persona' => 'Anmelden mit Persona',
 		'login_persona_problem' => 'Verbindungsproblem mit Persona?',
 		'login_persona_problem' => 'Verbindungsproblem mit Persona?',
 		'logout' => 'Abmelden',
 		'logout' => 'Abmelden',
-		'password' => 'Passwort',
+		'password' => array(
+			'_' => 'Passwort',
+			'format' => '<small>mindestens 7 Zeichen</small>',
+		),
+		'registration' => array(
+			'_' => 'Neuer Account',
+			'ask' => 'Erstelle einen Account?',
+			'title' => 'Accounterstellung',
+		),
 		'reset' => 'Zurücksetzen der Authentifizierung',
 		'reset' => 'Zurücksetzen der Authentifizierung',
-		'username' => 'Nutzername',
-		'username_admin' => 'Administrator-Nutzername',
+		'username' => array(
+			'_' => 'Nutzername',
+			'admin' => 'Administrator-Nutzername',
+			'format' => '<small>maximal 16 alphanumerische Zeichen</small>',
+		),
 		'will_reset' => 'Authentifikationssystem wird zurückgesetzt: ein Formular wird anstelle von Persona benutzt.',
 		'will_reset' => 'Authentifikationssystem wird zurückgesetzt: ein Formular wird anstelle von Persona benutzt.',
 	),
 	),
 	'date' => array(
 	'date' => array(

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

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 	'action' => array(
 		'finish' => 'Installation fertigstellen',
 		'finish' => 'Installation fertigstellen',
 		'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
 		'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
+		'keep_install' => 'Vorherige Installation beibehalten (Daten)',
 		'next_step' => 'Zum nächsten Schritt springen',
 		'next_step' => 'Zum nächsten Schritt springen',
+		'reinstall' => 'Neuinstallation von FreshRSS',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
 		'email_persona' => 'Anmelde-E-Mail-Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'email_persona' => 'Anmelde-E-Mail-Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
@@ -31,6 +33,7 @@ return array(
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Überprüfungen',
 		'_' => 'Überprüfungen',
+		'already_installed' => 'Wir haben festgestellt, dass FreshRSS bereits installiert wurde!',
 		'cache' => array(
 		'cache' => array(
 			'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/cache</em>. Der HTTP-Server muss Schreibrechte besitzen.',
 			'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/cache</em>. Der HTTP-Server muss Schreibrechte besitzen.',
 			'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/cache</em> sind in Ordnung.',
 			'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/cache</em> sind in Ordnung.',
@@ -93,6 +96,9 @@ return array(
 	'delete_articles_after' => 'Entferne Artikel nach',
 	'delete_articles_after' => 'Entferne Artikel nach',
 	'fix_errors_before' => 'Bitte den Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
 	'fix_errors_before' => 'Bitte den Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
 	'javascript_is_better' => 'FreshRSS ist ansprechender mit aktiviertem JavaScript',
 	'javascript_is_better' => 'FreshRSS ist ansprechender mit aktiviertem JavaScript',
+	'js' => array(
+		'confirm_reinstall' => 'Du wirst deine vorherige Konfiguration (Daten) verlieren FreshRSS. Bist du sicher, dass du fortfahren willst?',
+	),
 	'language' => array(
 	'language' => array(
 		'_' => 'Sprache',
 		'_' => 'Sprache',
 		'choose' => 'Wählen Sie eine Sprache für FreshRSS',
 		'choose' => 'Wählen Sie eine Sprache für FreshRSS',

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

@@ -37,6 +37,7 @@ return array(
 		'url' => 'Feed-URL',
 		'url' => 'Feed-URL',
 		'validator' => 'Überprüfen Sie die Gültigkeit des Feeds',
 		'validator' => 'Überprüfen Sie die Gültigkeit des Feeds',
 		'website' => 'Webseiten-URL',
 		'website' => 'Webseiten-URL',
+		'pubsubhubbub' => 'Sofortbenachrichtigung mit PubSubHubbub',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
 		'export' => 'Exportieren',
 		'export' => 'Exportieren',

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

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Create new user',
 		'create' => 'Create new user',
 		'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',
+		'number' => 'There is %d account created yet',
+		'numbers' => 'There are %d accounts created yet',
 		'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',
 		'password_format' => 'At least 7 characters',
+		'registration' => array(
+			'allow' => 'Allow account creation',
+			'help' => '0 means that there is no account limit',
+			'number' => 'Max number of accounts',
+		),
 		'title' => 'Manage users',
 		'title' => 'Manage users',
 		'user_list' => 'List of users',
 		'user_list' => 'List of users',
 		'username' => 'Username',
 		'username' => 'Username',

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

@@ -72,6 +72,10 @@ return array(
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'_' => 'Profile management',
 		'_' => 'Profile management',
+		'delete' => array(
+			'_' => 'Account deletion',
+			'warn' => 'Your account and all the related data will be deleted.',
+		),
 		'email_persona' => 'Login email address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'email_persona' => 'Login email 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>',

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

@@ -102,6 +102,7 @@ return array(
 			'_' => 'User %s has been deleted',
 			'_' => 'User %s has been deleted',
 			'error' => 'User %s cannot be deleted',
 			'error' => 'User %s cannot be deleted',
 		),
 		),
+		'set_registration' => 'The maximum amount of accounts has been updated.',
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'error' => 'Your profile cannot be modified',
 		'error' => 'Your profile cannot be modified',

+ 15 - 3
app/i18n/en/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Delete all articles',
 		'truncate' => 'Delete all articles',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'email' => 'Email address',
 		'keep_logged_in' => 'Keep me logged in <small>(1 month)</small>',
 		'keep_logged_in' => 'Keep me logged in <small>(1 month)</small>',
 		'login' => 'Login',
 		'login' => 'Login',
 		'login_persona' => 'Login with Persona',
 		'login_persona' => 'Login with Persona',
 		'login_persona_problem' => 'Connection problem with Persona?',
 		'login_persona_problem' => 'Connection problem with Persona?',
 		'logout' => 'Logout',
 		'logout' => 'Logout',
-		'password' => 'Password',
+		'password' => array(
+			'_' => 'Password',
+			'format' => '<small>At least 7 characters</small>',
+		),
+		'registration' => array(
+			'_' => 'New account',
+			'ask' => 'Create an account?',
+			'title' => 'Account creation',
+		),
 		'reset' => 'Authentication reset',
 		'reset' => 'Authentication reset',
-		'username' => 'Username',
-		'username_admin' => 'Administrator username',
+		'username' => array(
+			'_' => 'Username',
+			'admin' => 'Administrator username',
+			'format' => '<small>maximum 16 alphanumeric characters</small>',
+		),
 		'will_reset' => 'Authentication system will be reset: a form will be used instead of Persona.',
 		'will_reset' => 'Authentication system will be reset: a form will be used instead of Persona.',
 	),
 	),
 	'date' => array(
 	'date' => array(

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

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 	'action' => array(
 		'finish' => 'Complete installation',
 		'finish' => 'Complete installation',
 		'fix_errors_before' => 'Please fix errors before skipping to the next step.',
 		'fix_errors_before' => 'Please fix errors before skipping to the next step.',
+		'keep_install' => 'Keep previous installation',
 		'next_step' => 'Go to the next step',
 		'next_step' => 'Go to the next step',
+		'reinstall' => 'Reinstall FreshRSS',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
 		'email_persona' => 'Login email address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'email_persona' => 'Login email address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
@@ -31,6 +33,7 @@ return array(
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Checks',
 		'_' => 'Checks',
+		'already_installed' => 'We have detected that FreshRSS is already installed!',
 		'cache' => array(
 		'cache' => array(
 			'nok' => 'Check permissions on <em>./data/cache</em> directory. HTTP server must have rights to write into',
 			'nok' => 'Check permissions on <em>./data/cache</em> directory. HTTP server must have rights to write into',
 			'ok' => 'Permissions on cache directory are good.',
 			'ok' => 'Permissions on cache directory are good.',
@@ -93,6 +96,9 @@ return array(
 	'delete_articles_after' => 'Remove articles after',
 	'delete_articles_after' => 'Remove articles after',
 	'fix_errors_before' => 'Please fix errors before skipping to the next step.',
 	'fix_errors_before' => 'Please fix errors before skipping to the next step.',
 	'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled',
 	'javascript_is_better' => 'FreshRSS is more pleasant with JavaScript enabled',
+	'js' => array(
+		'confirm_reinstall' => 'You will lose your previous configuration by reinstalling FreshRSS. Are you sure you want to continue?',
+	),
 	'language' => array(
 	'language' => array(
 		'_' => 'Language',
 		'_' => 'Language',
 		'choose' => 'Choose a language for FreshRSS',
 		'choose' => 'Choose a language for FreshRSS',

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

@@ -37,6 +37,7 @@ return array(
 		'url' => 'Feed URL',
 		'url' => 'Feed URL',
 		'validator' => 'Check the validity of the feed',
 		'validator' => 'Check the validity of the feed',
 		'website' => 'Website URL',
 		'website' => 'Website URL',
+		'pubsubhubbub' => 'Instant notification with PubSubHubbub',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
 		'export' => 'Export',
 		'export' => 'Export',

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

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Créer un nouvel utilisateur',
 		'create' => 'Créer un nouvel utilisateur',
 		'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',
+		'number' => '%d compte a déjà été créé',
+		'numbers' => '%d comptes ont déjà été créés',
 		'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',
 		'password_format' => '7 caractères minimum',
+		'registration' => array(
+			'allow' => 'Autoriser la création de comptes',
+			'help' => 'Un chiffre de 0 signifie que l’on peut créer un nombre infini de comptes',
+			'number' => 'Nombre max de comptes',
+		),
 		'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',

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

@@ -72,6 +72,10 @@ return array(
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'_' => 'Gestion du profil',
 		'_' => 'Gestion du profil',
+		'delete' => array(
+			'_' => 'Suppression du compte',
+			'warn' => 'Le compte et toutes les données associées vont être supprimées.',
+		),
 		'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>',

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

@@ -102,6 +102,7 @@ return array(
 			'_' => 'L’utilisateur %s a été supprimé.',
 			'_' => 'L’utilisateur %s a été supprimé.',
 			'error' => 'L’utilisateur %s ne peut pas être supprimé.',
 			'error' => 'L’utilisateur %s ne peut pas être supprimé.',
 		),
 		),
+		'set_registration' => 'Le nombre maximal de comptes a été mis à jour.',
 	),
 	),
 	'profile' => array(
 	'profile' => array(
 		'error' => 'Votre profil n’a pas pu être mis à jour',
 		'error' => 'Votre profil n’a pas pu être mis à jour',

+ 15 - 3
app/i18n/fr/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Supprimer tous les articles',
 		'truncate' => 'Supprimer tous les articles',
 	),
 	),
 	'auth' => array(
 	'auth' => array(
+		'email' => 'Adresse courriel',
 		'keep_logged_in' => 'Rester connecté <small>(1 mois)</small>',
 		'keep_logged_in' => 'Rester connecté <small>(1 mois)</small>',
 		'login' => 'Connexion',
 		'login' => 'Connexion',
 		'login_persona' => 'Connexion avec Persona',
 		'login_persona' => 'Connexion avec Persona',
 		'login_persona_problem' => 'Problème de connexion à Persona ?',
 		'login_persona_problem' => 'Problème de connexion à Persona ?',
 		'logout' => 'Déconnexion',
 		'logout' => 'Déconnexion',
-		'password' => 'Mot de passe',
+		'password' => array(
+			'_' => 'Mot de passe',
+			'format' => '<small>7 caractères minimum</small>',
+		),
+		'registration' => array(
+			'_' => 'Nouveau compte',
+			'ask' => 'Créer un compte ?',
+			'title' => 'Création de compte',
+		),
 		'reset' => 'Réinitialisation de l’authentification',
 		'reset' => 'Réinitialisation de l’authentification',
-		'username' => 'Nom d’utilisateur',
-		'username_admin' => 'Nom d’utilisateur administrateur',
+		'username' => array(
+			'_' => 'Nom d’utilisateur',
+			'admin' => 'Nom d’utilisateur administrateur',
+			'format' => '<small>16 caractères alphanumériques maximum</small>',
+		),
 		'will_reset' => 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.',
 		'will_reset' => 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.',
 	),
 	),
 	'date' => array(
 	'date' => array(

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

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 	'action' => array(
 		'finish' => 'Terminer l’installation',
 		'finish' => 'Terminer l’installation',
 		'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.',
 		'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.',
+		'keep_install' => 'Garder l’ancienne configuration',
 		'next_step' => 'Passer à l’étape suivante',
 		'next_step' => 'Passer à l’étape suivante',
+		'reinstall' => 'Réinstaller FreshRSS',
 	),
 	),
 	'auth' => array(
 	'auth' => 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>',
@@ -31,6 +33,7 @@ return array(
 	),
 	),
 	'check' => array(
 	'check' => array(
 		'_' => 'Vérifications',
 		'_' => 'Vérifications',
+		'already_installed' => 'FreshRSS semble avoir déjà été installé !',
 		'cache' => array(
 		'cache' => array(
 			'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/cache</em>. Le serveur HTTP doit être capable d’écrire dedans',
 			'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/cache</em>. Le serveur HTTP doit être capable d’écrire dedans',
 			'ok' => 'Les droits sur le répertoire de cache sont bons.',
 			'ok' => 'Les droits sur le répertoire de cache sont bons.',
@@ -93,6 +96,9 @@ return array(
 	'delete_articles_after' => 'Supprimer les articles après',
 	'delete_articles_after' => 'Supprimer les articles après',
 	'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.',
 	'fix_errors_before' => 'Veuillez corriger les erreurs avant de passer à l’étape suivante.',
 	'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé',
 	'javascript_is_better' => 'FreshRSS est plus agréable à utiliser avec JavaScript activé',
+	'js' => array(
+		'confirm_reinstall' => 'Réinstaller FreshRSS vous fera perdre la configuration précédente. Êtes-vous sûr de vouloir continuer ?',
+	),
 	'language' => array(
 	'language' => array(
 		'_' => 'Langue',
 		'_' => 'Langue',
 		'choose' => 'Choisissez la langue pour FreshRSS',
 		'choose' => 'Choisissez la langue pour FreshRSS',

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

@@ -35,8 +35,9 @@ return array(
 		'title_add' => 'Ajouter un flux RSS',
 		'title_add' => 'Ajouter un flux RSS',
 		'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
 		'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
 		'url' => 'URL du flux',
 		'url' => 'URL du flux',
-		'validator' => 'Vérifier la valididé du flux',
+		'validator' => 'Vérifier la validité du flux',
 		'website' => 'URL du site',
 		'website' => 'URL du site',
+		'pubsubhubbub' => 'Notification instantanée par PubSubHubbub',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
 		'export' => 'Exporter',
 		'export' => 'Exporter',

+ 141 - 41
app/install.php

@@ -9,6 +9,9 @@ session_name('FreshRSS');
 session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
 session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
 session_start();
 session_start();
 
 
+Minz_Configuration::register('default_system', join_path(DATA_PATH, 'config.default.php'));
+Minz_Configuration::register('default_user', join_path(USERS_PATH, '_', 'config.default.php'));
+
 if (isset($_GET['step'])) {
 if (isset($_GET['step'])) {
 	define('STEP',(int)$_GET['step']);
 	define('STEP',(int)$_GET['step']);
 } else {
 } else {
@@ -76,10 +79,51 @@ function saveLanguage() {
 	}
 	}
 }
 }
 
 
+function saveStep1() {
+	if (isset($_POST['freshrss-keep-install']) &&
+			$_POST['freshrss-keep-install'] === '1') {
+		// We want to keep our previous installation of FreshRSS
+		// so we need to make next steps valid by setting $_SESSION vars
+		// with values from the previous installation
+
+		// First, we try to get previous configurations
+		Minz_Configuration::register('system',
+		                             join_path(DATA_PATH, 'config.php'),
+		                             join_path(DATA_PATH, 'config.default.php'));
+		$system_conf = Minz_Configuration::get('system');
+
+		$current_user = $system_conf->default_user;
+		Minz_Configuration::register('user',
+		                             join_path(USERS_PATH, $current_user, 'config.php'),
+		                             join_path(USERS_PATH, '_', 'config.default.php'));
+		$user_conf = Minz_Configuration::get('user');
+
+		// Then, we set $_SESSION vars
+		$_SESSION['title'] = $system_conf->title;
+		$_SESSION['auth_type'] = $system_conf->auth_type;
+		$_SESSION['old_entries'] = $user_conf->old_entries;
+		$_SESSION['mail_login'] = $user_conf->mail_login;
+		$_SESSION['default_user'] = $current_user;
+		$_SESSION['passwordHash'] = $user_conf->passwordHash;
+
+		$db = $system_conf->db;
+		$_SESSION['bd_type'] = $db['type'];
+		$_SESSION['bd_host'] = $db['host'];
+		$_SESSION['bd_user'] = $db['user'];
+		$_SESSION['bd_password'] = $db['password'];
+		$_SESSION['bd_base'] = $db['base'];
+		$_SESSION['bd_prefix'] = $db['prefix'];
+		$_SESSION['bd_error'] = '';
+
+		header('Location: index.php?step=4');
+	}
+}
+
 function saveStep2() {
 function saveStep2() {
+	$user_default_config = Minz_Configuration::get('default_user');
 	if (!empty($_POST)) {
 	if (!empty($_POST)) {
 		$_SESSION['title'] = substr(trim(param('title', _t('gen.freshrss'))), 0, 25);
 		$_SESSION['title'] = substr(trim(param('title', _t('gen.freshrss'))), 0, 25);
-		$_SESSION['old_entries'] = param('old_entries', 3);
+		$_SESSION['old_entries'] = param('old_entries', $user_default_config->old_entries);
 		$_SESSION['auth_type'] = param('auth_type', 'form');
 		$_SESSION['auth_type'] = param('auth_type', 'form');
 		$_SESSION['default_user'] = substr(preg_replace('/[^a-zA-Z0-9]/', '', param('default_user', '')), 0, 16);
 		$_SESSION['default_user'] = substr(preg_replace('/[^a-zA-Z0-9]/', '', param('default_user', '')), 0, 16);
 		$_SESSION['mail_login'] = filter_var(param('mail_login', ''), FILTER_VALIDATE_EMAIL);
 		$_SESSION['mail_login'] = filter_var(param('mail_login', ''), FILTER_VALIDATE_EMAIL);
@@ -108,7 +152,7 @@ function saveStep2() {
 
 
 		$_SESSION['salt'] = sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
 		$_SESSION['salt'] = sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
 		if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
 		if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
-			$_SESSION['old_entries'] = 3;
+			$_SESSION['old_entries'] = $user_default_config->old_entries;
 		}
 		}
 
 
 		$token = '';
 		$token = '';
@@ -118,7 +162,7 @@ function saveStep2() {
 
 
 		$config_array = array(
 		$config_array = array(
 			'language' => $_SESSION['language'],
 			'language' => $_SESSION['language'],
-			'theme' => 'Origine',
+			'theme' => $user_default_config->theme,
 			'old_entries' => $_SESSION['old_entries'],
 			'old_entries' => $_SESSION['old_entries'],
 			'mail_login' => $_SESSION['mail_login'],
 			'mail_login' => $_SESSION['mail_login'],
 			'passwordHash' => $_SESSION['passwordHash'],
 			'passwordHash' => $_SESSION['passwordHash'],
@@ -165,14 +209,14 @@ function saveStep3() {
 			$_SESSION['bd_user'] = $_POST['user'];
 			$_SESSION['bd_user'] = $_POST['user'];
 			$_SESSION['bd_password'] = $_POST['pass'];
 			$_SESSION['bd_password'] = $_POST['pass'];
 			$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
 			$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
-			$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] .(empty($_SESSION['default_user']) ? '' :($_SESSION['default_user'] . '_'));
+			$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_'));
 		}
 		}
 
 
-		//TODO: load `config.default.php` as default
+		// We use dirname to remove the /i part
+		$base_url = dirname(Minz_Request::guessBaseUrl());
 		$config_array = array(
 		$config_array = array(
-			'environment' => 'production',
-			'simplepie_syslog_enabled' => true,
 			'salt' => $_SESSION['salt'],
 			'salt' => $_SESSION['salt'],
+			'base_url' => $base_url,
 			'title' => $_SESSION['title'],
 			'title' => $_SESSION['title'],
 			'default_user' => $_SESSION['default_user'],
 			'default_user' => $_SESSION['default_user'],
 			'auth_type' => $_SESSION['auth_type'],
 			'auth_type' => $_SESSION['auth_type'],
@@ -183,7 +227,9 @@ function saveStep3() {
 				'password' => $_SESSION['bd_password'],
 				'password' => $_SESSION['bd_password'],
 				'base' => $_SESSION['bd_base'],
 				'base' => $_SESSION['bd_base'],
 				'prefix' => $_SESSION['bd_prefix'],
 				'prefix' => $_SESSION['bd_prefix'],
+				'pdo_options' => array(),
 			),
 			),
+			'pubsubhubbub_enabled' => server_is_public($base_url),
 		);
 		);
 
 
 		@unlink(join_path(DATA_PATH, 'config.php'));	//To avoid access-rights problems
 		@unlink(join_path(DATA_PATH, 'config.php'));	//To avoid access-rights problems
@@ -300,6 +346,33 @@ function checkStep1() {
 	);
 	);
 }
 }
 
 
+function freshrss_already_installed() {
+	$conf_path = join_path(DATA_PATH, 'config.php');
+	if (!file_exists($conf_path)) {
+		return false;
+	}
+
+	// A configuration file already exists, we try to load it.
+	$system_conf = null;
+	try {
+		Minz_Configuration::register('system', $conf_path);
+		$system_conf = Minz_Configuration::get('system');
+	} catch (Minz_FileNotExistException $e) {
+		return false;
+	}
+
+	// ok, the global conf exists... but what about default user conf?
+	$current_user = $system_conf->default_user;
+	try {
+		Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php'));
+	} catch (Minz_FileNotExistException $e) {
+		return false;
+	}
+
+	// ok, ok, default user exists too!
+	return true;
+}
+
 function checkStep2() {
 function checkStep2() {
 	$conf = !empty($_SESSION['title']) &&
 	$conf = !empty($_SESSION['title']) &&
 	        !empty($_SESSION['old_entries']) &&
 	        !empty($_SESSION['old_entries']) &&
@@ -427,7 +500,7 @@ function printStep0() {
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="language"><?php echo _t('install.language'); ?></label>
 			<label class="group-name" for="language"><?php echo _t('install.language'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<select name="language" id="language">
+				<select name="language" id="language" tabindex="1" >
 				<?php foreach ($languages as $lang) { ?>
 				<?php foreach ($languages as $lang) { ?>
 				<option value="<?php echo $lang; ?>"<?php echo $actual == $lang ? ' selected="selected"' : ''; ?>>
 				<option value="<?php echo $lang; ?>"<?php echo $actual == $lang ? ' selected="selected"' : ''; ?>>
 					<?php echo _t('gen.lang.' . $lang); ?>
 					<?php echo _t('gen.lang.' . $lang); ?>
@@ -439,10 +512,10 @@ function printStep0() {
 
 
 		<div class="form-group form-actions">
 		<div class="form-group form-actions">
 			<div class="group-controls">
 			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
-				<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
+				<button type="submit" class="btn btn-important" tabindex="2" ><?php echo _t('gen.action.submit'); ?></button>
+				<button type="reset" class="btn" tabindex="3" ><?php echo _t('gen.action.cancel'); ?></button>
 				<?php if ($s0['all'] == 'ok') { ?>
 				<?php if ($s0['all'] == 'ok') { ?>
-				<a class="btn btn-important next-step" href="?step=1"><?php echo _t('install.action.next_step'); ?></a>
+				<a class="btn btn-important next-step" href="?step=1" tabindex="4" ><?php echo _t('install.action.next_step'); ?></a>
 				<?php } ?>
 				<?php } ?>
 			</div>
 			</div>
 		</div>
 		</div>
@@ -535,8 +608,38 @@ function printStep1() {
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.check.http_referer.nok'); ?></p>
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.check.http_referer.nok'); ?></p>
 	<?php } ?>
 	<?php } ?>
 
 
-	<?php if ($res['all'] == 'ok') { ?>
-	<a class="btn btn-important next-step" href="?step=2"><?php echo _t('install.action.next_step'); ?></a>
+	<?php if (freshrss_already_installed() && $res['all'] == 'ok') { ?>
+	<p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.attention'); ?></span> <?php echo _t('install.check.already_installed'); ?></p>
+
+	<form action="index.php?step=1" method="post">
+		<input type="hidden" name="freshrss-keep-install" value="1" />
+		<button type="submit" class="btn btn-important next-step" tabindex="1" ><?php echo _t('install.action.keep_install'); ?></button>
+		<a class="btn btn-attention next-step confirm" data-str-confirm="<?php echo _t('install.js.confirm_reinstall'); ?>" href="?step=2" tabindex="2" ><?php echo _t('install.action.reinstall'); ?></a>
+	</form>
+
+	<script>
+		function ask_confirmation(e) {
+			var str_confirmation = this.getAttribute('data-str-confirm');
+			if (!str_confirmation) {
+				str_confirmation = "<?php echo _t('gen.js.confirm_action'); ?>";
+			}
+
+			if (!confirm(str_confirmation)) {
+				e.preventDefault();
+			}
+		}
+
+		function init_confirm() {
+			confirms = document.getElementsByClassName('confirm');
+			for (var i = 0 ; i < confirms.length ; i++) {
+				confirms[i].addEventListener('click', ask_confirmation);
+			}
+		}
+
+		init_confirm();
+	</script>
+	<?php } elseif ($res['all'] == 'ok') { ?>
+	<a class="btn btn-important next-step" href="?step=2" tabindex="1" ><?php echo _t('install.action.next_step'); ?></a>
 	<?php } else { ?>
 	<?php } else { ?>
 	<p class="alert alert-error"><?php echo _t('install.action.fix_errors_before'); ?></p>
 	<p class="alert alert-error"><?php echo _t('install.action.fix_errors_before'); ?></p>
 	<?php } ?>
 	<?php } ?>
@@ -544,6 +647,7 @@ function printStep1() {
 }
 }
 
 
 function printStep2() {
 function printStep2() {
+	$user_default_config = Minz_Configuration::get('default_user');
 ?>
 ?>
 	<?php $s2 = checkStep2(); if ($s2['all'] == 'ok') { ?>
 	<?php $s2 = checkStep2(); if ($s2['all'] == 'ok') { ?>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t('gen.short.ok'); ?></span> <?php echo _t('install.conf.ok'); ?></p>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t('gen.short.ok'); ?></span> <?php echo _t('install.conf.ok'); ?></p>
@@ -557,28 +661,28 @@ function printStep2() {
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="title"><?php echo _t('install.title'); ?></label>
 			<label class="group-name" for="title"><?php echo _t('install.title'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" id="title" name="title" value="<?php echo isset($_SESSION['title']) ? $_SESSION['title'] : _t('gen.freshrss'); ?>" />
+				<input type="text" id="title" name="title" value="<?php echo isset($_SESSION['title']) ? $_SESSION['title'] : _t('gen.freshrss'); ?>" tabindex="1" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="old_entries"><?php echo _t('install.delete_articles_after'); ?></label>
 			<label class="group-name" for="old_entries"><?php echo _t('install.delete_articles_after'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="number" id="old_entries" name="old_entries" required="required" min="1" max="1200" value="<?php echo isset($_SESSION['old_entries']) ? $_SESSION['old_entries'] : '3'; ?>" /> <?php echo _t('gen.date.month'); ?>
+				<input type="number" id="old_entries" name="old_entries" required="required" min="1" max="1200" value="<?php echo isset($_SESSION['old_entries']) ? $_SESSION['old_entries'] : $user_default_config->old_entries; ?>" tabindex="2" /> <?php echo _t('gen.date.month'); ?>
 			</div>
 			</div>
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="default_user"><?php echo _t('install.default_user'); ?></label>
 			<label class="group-name" for="default_user"><?php echo _t('install.default_user'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" id="default_user" name="default_user" required="required" size="16" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" value="<?php echo isset($_SESSION['default_user']) ? $_SESSION['default_user'] : ''; ?>" placeholder="<?php echo httpAuthUser() == '' ? 'alice' : httpAuthUser(); ?>" />
+				<input type="text" id="default_user" name="default_user" required="required" size="16" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" value="<?php echo isset($_SESSION['default_user']) ? $_SESSION['default_user'] : ''; ?>" placeholder="<?php echo httpAuthUser() == '' ? 'alice' : httpAuthUser(); ?>" tabindex="3" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="auth_type"><?php echo _t('install.auth.type'); ?></label>
 			<label class="group-name" for="auth_type"><?php echo _t('install.auth.type'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<select id="auth_type" name="auth_type" required="required" onchange="auth_type_change(true)">
+				<select id="auth_type" name="auth_type" required="required" onchange="auth_type_change(true)" tabindex="4">
 					<?php
 					<?php
 						function no_auth($auth_type) {
 						function no_auth($auth_type) {
 							return !in_array($auth_type, array('form', 'persona', 'http_auth', 'none'));
 							return !in_array($auth_type, array('form', 'persona', 'http_auth', 'none'));
@@ -597,7 +701,7 @@ function printStep2() {
 			<label class="group-name" for="passwordPlain"><?php echo _t('install.auth.password_form'); ?></label>
 			<label class="group-name" for="passwordPlain"><?php echo _t('install.auth.password_form'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
 				<div class="stick">
 				<div class="stick">
-					<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"' : ''; ?> tabindex="5" />
 					<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'); ?>
 				<?php echo _i('help'); ?> <?php echo _t('install.auth.password_format'); ?>
@@ -608,7 +712,7 @@ function printStep2() {
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="mail_login"><?php echo _t('install.auth.email_persona'); ?></label>
 			<label class="group-name" for="mail_login"><?php echo _t('install.auth.email_persona'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="email" id="mail_login" name="mail_login" value="<?php echo isset($_SESSION['mail_login']) ? $_SESSION['mail_login'] : ''; ?>" placeholder="alice@example.net" <?php echo $auth_type === 'persona' ? ' required="required"' : ''; ?> />
+				<input type="email" id="mail_login" name="mail_login" value="<?php echo isset($_SESSION['mail_login']) ? $_SESSION['mail_login'] : ''; ?>" placeholder="alice@example.net" <?php echo $auth_type === 'persona' ? ' required="required"' : ''; ?> tabindex="6"/>
 				<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>
@@ -636,7 +740,7 @@ function printStep2() {
 				toggles[i].addEventListener('mouseup', hide_password);
 				toggles[i].addEventListener('mouseup', hide_password);
 			}
 			}
 
 
-			function auth_type_change(focus) {
+			function auth_type_change() {
 				var auth_value = document.getElementById('auth_type').value,
 				var auth_value = document.getElementById('auth_type').value,
 				    password_input = document.getElementById('passwordPlain'),
 				    password_input = document.getElementById('passwordPlain'),
 				    mail_input = document.getElementById('mail_login');
 				    mail_input = document.getElementById('mail_login');
@@ -644,29 +748,23 @@ function printStep2() {
 				if (auth_value === 'form') {
 				if (auth_value === 'form') {
 					password_input.required = true;
 					password_input.required = true;
 					mail_input.required = false;
 					mail_input.required = false;
-					if (focus) {
-						password_input.focus();
-					}
 				} else if (auth_value === 'persona') {
 				} else if (auth_value === 'persona') {
 					password_input.required = false;
 					password_input.required = false;
 					mail_input.required = true;
 					mail_input.required = true;
-					if (focus) {
-						mail_input.focus();
-					}
 				} else {
 				} else {
 					password_input.required = false;
 					password_input.required = false;
 					mail_input.required = false;
 					mail_input.required = false;
 				}
 				}
 			}
 			}
-			auth_type_change(false);
+			auth_type_change();
 		</script>
 		</script>
 
 
 		<div class="form-group form-actions">
 		<div class="form-group form-actions">
 			<div class="group-controls">
 			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
-				<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
+				<button type="submit" class="btn btn-important" tabindex="7" ><?php echo _t('gen.action.submit'); ?></button>
+				<button type="reset" class="btn" tabindex="8" ><?php echo _t('gen.action.cancel'); ?></button>
 				<?php if ($s2['all'] == 'ok') { ?>
 				<?php if ($s2['all'] == 'ok') { ?>
-				<a class="btn btn-important next-step" href="?step=3"><?php echo _t('install.action.next_step'); ?></a>
+				<a class="btn btn-important next-step" href="?step=3" tabindex="9" ><?php echo _t('install.action.next_step'); ?></a>
 				<?php } ?>
 				<?php } ?>
 			</div>
 			</div>
 		</div>
 		</div>
@@ -675,6 +773,7 @@ function printStep2() {
 }
 }
 
 
 function printStep3() {
 function printStep3() {
+	$system_default_config = Minz_Configuration::get('default_system');
 ?>
 ?>
 	<?php $s3 = checkStep3(); if ($s3['all'] == 'ok') { ?>
 	<?php $s3 = checkStep3(); if ($s3['all'] == 'ok') { ?>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t('gen.short.ok'); ?></span> <?php echo _t('install.bdd.conf.ok'); ?></p>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t('gen.short.ok'); ?></span> <?php echo _t('install.bdd.conf.ok'); ?></p>
@@ -687,7 +786,7 @@ function printStep3() {
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
 			<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<select name="type" id="type" onchange="mySqlShowHide()">
+				<select name="type" id="type" onchange="mySqlShowHide()" tabindex="1" >
 				<?php if (extension_loaded('pdo_mysql')) {?>
 				<?php if (extension_loaded('pdo_mysql')) {?>
 				<option value="mysql"
 				<option value="mysql"
 					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
 					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
@@ -708,35 +807,35 @@ function printStep3() {
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="host"><?php echo _t('install.bdd.host'); ?></label>
 			<label class="group-name" for="host"><?php echo _t('install.bdd.host'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : 'localhost'; ?>" />
+				<input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="user"><?php echo _t('install.bdd.username'); ?></label>
 			<label class="group-name" for="user"><?php echo _t('install.bdd.username'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" id="user" name="user" maxlength="16" pattern="[0-9A-Za-z_.-]{1,16}" value="<?php echo isset($_SESSION['bd_user']) ? $_SESSION['bd_user'] : ''; ?>" />
+				<input type="text" id="user" name="user" maxlength="16" pattern="[0-9A-Za-z_.-]{1,16}" value="<?php echo isset($_SESSION['bd_user']) ? $_SESSION['bd_user'] : ''; ?>" tabindex="3" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label>
 			<label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" />
+				<input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="base"><?php echo _t('install.bdd'); ?></label>
 			<label class="group-name" for="base"><?php echo _t('install.bdd'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" id="base" name="base" maxlength="64" pattern="[0-9A-Za-z_]{1,64}" value="<?php echo isset($_SESSION['bd_base']) ? $_SESSION['bd_base'] : ''; ?>" />
+				<input type="text" id="base" name="base" maxlength="64" pattern="[0-9A-Za-z_]{1,64}" value="<?php echo isset($_SESSION['bd_base']) ? $_SESSION['bd_base'] : ''; ?>" tabindex="5" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="prefix"><?php echo _t('install.bdd.prefix'); ?></label>
 			<label class="group-name" for="prefix"><?php echo _t('install.bdd.prefix'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" id="prefix" name="prefix" maxlength="16" pattern="[0-9A-Za-z_]{1,16}" value="<?php echo isset($_SESSION['bd_prefix']) ? $_SESSION['bd_prefix'] : 'freshrss_'; ?>" />
+				<input type="text" id="prefix" name="prefix" maxlength="16" pattern="[0-9A-Za-z_]{1,16}" value="<?php echo isset($_SESSION['bd_prefix']) ? $_SESSION['bd_prefix'] :  $system_default_config->db['prefix']; ?>" tabindex="6" />
 			</div>
 			</div>
 		</div>
 		</div>
 		</div>
 		</div>
@@ -756,10 +855,10 @@ function printStep3() {
 
 
 		<div class="form-group form-actions">
 		<div class="form-group form-actions">
 			<div class="group-controls">
 			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
-				<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
+				<button type="submit" class="btn btn-important" tabindex="7" ><?php echo _t('gen.action.submit'); ?></button>
+				<button type="reset" class="btn" tabindex="8" ><?php echo _t('gen.action.cancel'); ?></button>
 				<?php if ($s3['all'] == 'ok') { ?>
 				<?php if ($s3['all'] == 'ok') { ?>
-				<a class="btn btn-important next-step" href="?step=4"><?php echo _t('install.action.next_step'); ?></a>
+				<a class="btn btn-important next-step" href="?step=4" tabindex="9" ><?php echo _t('install.action.next_step'); ?></a>
 				<?php } ?>
 				<?php } ?>
 			</div>
 			</div>
 		</div>
 		</div>
@@ -770,7 +869,7 @@ function printStep3() {
 function printStep4() {
 function printStep4() {
 ?>
 ?>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t('install.congratulations'); ?></span> <?php echo _t('install.ok'); ?></p>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t('install.congratulations'); ?></span> <?php echo _t('install.ok'); ?></p>
-	<a class="btn btn-important next-step" href="?step=5"><?php echo _t('install.action.finish'); ?></a>
+	<a class="btn btn-important next-step" href="?step=5" tabindex="1"><?php echo _t('install.action.finish'); ?></a>
 <?php
 <?php
 }
 }
 
 
@@ -790,6 +889,7 @@ default:
 	saveLanguage();
 	saveLanguage();
 	break;
 	break;
 case 1:
 case 1:
+	saveStep1();
 	break;
 	break;
 case 2:
 case 2:
 	saveStep2();
 	saveStep2();
@@ -829,7 +929,7 @@ case 5:
 		<li class="item<?php echo STEP == 1 ? ' active' : ''; ?>"><a href="?step=1"><?php echo _t('install.check'); ?></a></li>
 		<li class="item<?php echo STEP == 1 ? ' active' : ''; ?>"><a href="?step=1"><?php echo _t('install.check'); ?></a></li>
 		<li class="item<?php echo STEP == 2 ? ' active' : ''; ?>"><a href="?step=2"><?php echo _t('install.conf'); ?></a></li>
 		<li class="item<?php echo STEP == 2 ? ' active' : ''; ?>"><a href="?step=2"><?php echo _t('install.conf'); ?></a></li>
 		<li class="item<?php echo STEP == 3 ? ' active' : ''; ?>"><a href="?step=3"><?php echo _t('install.bdd.conf'); ?></a></li>
 		<li class="item<?php echo STEP == 3 ? ' active' : ''; ?>"><a href="?step=3"><?php echo _t('install.bdd.conf'); ?></a></li>
-		<li class="item<?php echo STEP == 4 ? ' active' : ''; ?>"><a href="?step=5"><?php echo _t('install.this_is_the_end'); ?></a></li>
+		<li class="item<?php echo STEP == 4 ? ' active' : ''; ?>"><a href="?step=4"><?php echo _t('install.this_is_the_end'); ?></a></li>
 	</ul>
 	</ul>
 
 
 	<div class="post">
 	<div class="post">

+ 4 - 0
app/views/auth/formLogin.phtml

@@ -1,6 +1,10 @@
 <div class="prompt">
 <div class="prompt">
 	<h1><?php echo _t('gen.auth.login'); ?></h1>
 	<h1><?php echo _t('gen.auth.login'); ?></h1>
 
 
+	<?php if (!max_registrations_reached()) { ?>
+		<a href="<?php echo _url('auth', 'register'); ?>"><?php echo _t('gen.auth.registration.ask'); ?></a>
+	<?php } ?>
+
 	<form id="crypto-form" method="post" action="<?php echo _url('auth', 'login'); ?>">
 	<form id="crypto-form" method="post" action="<?php echo _url('auth', 'login'); ?>">
 		<div>
 		<div>
 			<label for="username"><?php echo _t('gen.auth.username'); ?></label>
 			<label for="username"><?php echo _t('gen.auth.username'); ?></label>

+ 4 - 0
app/views/auth/personaLogin.phtml

@@ -2,6 +2,10 @@
 <div class="prompt">
 <div class="prompt">
 	<h1><?php echo _t('gen.auth.login'); ?></h1>
 	<h1><?php echo _t('gen.auth.login'); ?></h1>
 
 
+	<?php if (!max_registrations_reached()) { ?>
+		<a href="<?php echo _url('auth', 'register'); ?>"><?php echo _t('gen.auth.registration.ask'); ?></a>
+	<?php } ?>
+
 	<p>
 	<p>
 		<a class="signin btn btn-important" href="<?php echo _url('auth', 'login'); ?>">
 		<a class="signin btn btn-important" href="<?php echo _url('auth', 'login'); ?>">
 			<?php echo _i('login'); ?> <?php echo _t('gen.auth.login_persona'); ?>
 			<?php echo _i('login'); ?> <?php echo _t('gen.auth.login_persona'); ?>

+ 38 - 0
app/views/auth/register.phtml

@@ -0,0 +1,38 @@
+<div class="prompt">
+    <h1><?php echo _t('gen.auth.registration'); ?></h1>
+
+    <form method="post" action="<?php echo _url('user', 'create'); ?>">
+        <div>
+            <label class="group-name" for="new_user_name"><?php echo _t('gen.auth.username'), '<br />', _i('help'), ' ', _t('gen.auth.username.format'); ?></label>
+            <input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" autocomplete="off" pattern="[0-9a-zA-Z]{1,16}" />
+        </div>
+
+        <div>
+            <label class="group-name" for="new_user_passwordPlain"><?php echo _t('gen.auth.password'), '<br />', _i('help'), ' ', _t('gen.auth.password.format'); ?></label>
+            <div class="stick">
+                <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" required="required" autocomplete="off" pattern=".{7,}" />
+                <a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
+            </div>
+            <noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
+        </div>
+
+        <div>
+            <label class="group-name" for="new_user_email"><?php echo _t('gen.auth.email'); ?></label>
+            <input type="email" id="new_user_email" name="new_user_email" class="extend" required="required" autocomplete="off" />
+        </div>
+
+        <div>
+            <?php
+                $redirect_url = urlencode(Minz_Url::display(
+                    array('c' => 'index', 'a' => 'index'),
+                    'php', true
+                ));
+            ?>
+            <input type="hidden" name="r" value="<?php echo $redirect_url; ?>" />
+            <button type="submit" class="btn btn-important"><?php echo _t('gen.action.create'); ?></button>
+            <a class="btn" href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.cancel'); ?></a>
+        </div>
+    </form>
+
+    <p><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('gen.freshrss.about'); ?></a></p>
+</div>

+ 1 - 1
app/views/auth/reset.phtml

@@ -16,7 +16,7 @@
 		</p>
 		</p>
 
 
 		<div>
 		<div>
-			<label for="username"><?php echo _t('gen.auth.username_admin'); ?></label>
+			<label for="username"><?php echo _t('gen.auth.username.admin'); ?></label>
 			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
 			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
 		</div>
 		</div>
 		<div>
 		<div>

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

@@ -107,7 +107,7 @@
 		</div>
 		</div>
 		
 		
 		<div class="form-group">
 		<div class="form-group">
-			<label class="group-name" for="posts_per_page"><?php echo _t('conf.display.notif_html5.timeout'); ?></label>
+			<label class="group-name" for="html5_notif_timeout"><?php echo _t('conf.display.notif_html5.timeout'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
 				<input type="number" id="html5_notif_timeout" name="html5_notif_timeout" value="<?php echo FreshRSS_Context::$user_conf->html5_notif_timeout; ?>" data-leave-validation="<?php echo FreshRSS_Context::$user_conf->html5_notif_timeout; ?>"/> <?php echo _t('conf.display.notif_html5.seconds'); ?>
 				<input type="number" id="html5_notif_timeout" name="html5_notif_timeout" value="<?php echo FreshRSS_Context::$user_conf->html5_notif_timeout; ?>" data-leave-validation="<?php echo FreshRSS_Context::$user_conf->html5_notif_timeout; ?>"/> <?php echo _t('conf.display.notif_html5.seconds'); ?>
 			</div>
 			</div>

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

@@ -9,7 +9,7 @@
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="posts_per_page"><?php echo _t('conf.reading.articles_per_page'); ?></label>
 			<label class="group-name" for="posts_per_page"><?php echo _t('conf.reading.articles_per_page'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo FreshRSS_Context::$user_conf->posts_per_page; ?>" min="5" max="50" data-leave-validation="<?php echo FreshRSS_Context::$user_conf->posts_per_page; ?>"/>
+				<input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo FreshRSS_Context::$user_conf->posts_per_page; ?>" min="5" max="500" data-leave-validation="<?php echo FreshRSS_Context::$user_conf->posts_per_page; ?>"/>
 				<?php echo _i('help'); ?> <?php echo _t('conf.reading.number_divided_when_reader'); ?>
 				<?php echo _i('help'); ?> <?php echo _t('conf.reading.number_divided_when_reader'); ?>
 			</div>
 			</div>
 		</div>
 		</div>

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

@@ -67,7 +67,7 @@
 		<div class="form-group">
 		<div class="form-group">
 			<label class="group-name" for="http_user"><?php echo _t('sub.feed.auth.username'); ?></label>
 			<label class="group-name" for="http_user"><?php echo _t('sub.feed.auth.username'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" name="http_user" id="http_user" class="extend" value="<?php echo $auth['username']; ?>" autocomplete="off" />
+				<input type="text" name="http_user" id="http_user" class="extend" value="<?php echo empty($auth['username']) ? ' ' : $auth['username']; ?>" autocomplete="off" />
 			</div>
 			</div>
 
 
 			<label class="group-name" for="http_pass"><?php echo _t('sub.feed.auth.password'); ?></label>
 			<label class="group-name" for="http_pass"><?php echo _t('sub.feed.auth.password'); ?></label>

+ 12 - 4
app/views/helpers/feed/update.phtml

@@ -126,6 +126,14 @@
 				?></select>
 				?></select>
 			</div>
 			</div>
 		</div>
 		</div>
+		<div class="form-group">
+			<label class="group-name" for="pubsubhubbub"><?php echo _t('sub.feed.pubsubhubbub'); ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="pubsubhubbub">
+					<input type="checkbox" name="pubsubhubbub" id="pubsubhubbub" disabled="disabled" value="1"<?php echo $this->feed->pubSubHubbubEnabled() ? ' checked="checked"' : ''; ?> />
+				</label>
+			</div>
+		</div>
 		<div class="form-group form-actions">
 		<div class="form-group form-actions">
 			<div class="group-controls">
 			<div class="group-controls">
 				<button class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
 				<button class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
@@ -136,15 +144,15 @@
 		<legend><?php echo _t('sub.feed.auth.configuration'); ?></legend>
 		<legend><?php echo _t('sub.feed.auth.configuration'); ?></legend>
 		<?php $auth = $this->feed->httpAuth(false); ?>
 		<?php $auth = $this->feed->httpAuth(false); ?>
 		<div class="form-group">
 		<div class="form-group">
-			<label class="group-name" for="http_user"><?php echo _t('sub.feed.auth.username'); ?></label>
+			<label class="group-name" for="http_user_feed<?php echo $this->feed->id(); ?>"><?php echo _t('sub.feed.auth.username'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="text" name="http_user" id="http_user" class="extend" value="<?php echo $auth['username']; ?>" autocomplete="off" />
+				<input type="text" name="http_user_feed<?php echo $this->feed->id(); ?>" id="http_user_feed<?php echo $this->feed->id(); ?>" class="extend" value="<?php echo empty($auth['username']) ? ' ' : $auth['username']; ?>" autocomplete="off" />
 				<?php echo _i('help'); ?> <?php echo _t('sub.feed.auth.help'); ?>
 				<?php echo _i('help'); ?> <?php echo _t('sub.feed.auth.help'); ?>
 			</div>
 			</div>
 
 
-			<label class="group-name" for="http_pass"><?php echo _t('sub.feed.auth.password'); ?></label>
+			<label class="group-name" for="http_pass_feed<?php echo $this->feed->id(); ?>"><?php echo _t('sub.feed.auth.password'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
-				<input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
+				<input type="password" name="http_pass_feed<?php echo $this->feed->id(); ?>" id="http_pass_feed<?php echo $this->feed->id(); ?>" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" />
 			</div>
 			</div>
 		</div>
 		</div>
 
 

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

@@ -36,10 +36,10 @@
 
 
 					<li class="dropdown-header"><?php echo _t('sub.feed.auth.http'); ?></li>
 					<li class="dropdown-header"><?php echo _t('sub.feed.auth.http'); ?></li>
 					<li class="input">
 					<li class="input">
-						<input type="text" name="http_user" id="http_user_add" autocomplete="off" placeholder="<?php echo _t('sub.feed.auth.username'); ?>" />
+						<input type="text" name="http_user" id="http_user_feed" value=" " autocomplete="off" placeholder="<?php echo _t('sub.feed.auth.username'); ?>" />
 					</li>
 					</li>
 					<li class="input">
 					<li class="input">
-						<input type="password" name="http_pass" id="http_pass_add" autocomplete="off" placeholder="<?php echo _t('sub.feed.auth.password'); ?>" />
+						<input type="password" name="http_pass" id="http_pass_feed" autocomplete="off" placeholder="<?php echo _t('sub.feed.auth.password'); ?>" />
 					</li>
 					</li>
 				</ul>
 				</ul>
 			</div>
 			</div>

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

@@ -3,6 +3,34 @@
 <div class="post">
 <div class="post">
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a>
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a>
 
 
+	<form method="post" action="<?php echo _url('user', 'setRegistration'); ?>">
+		<legend><?php echo _t('admin.user.registration.allow'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="max-registrations"><?php echo _t('admin.user.registration.number'); ?></label>
+			<div class="group-controls">
+				<input type="number" id="max-registrations" name="max-registrations" value="<?php echo FreshRSS_Context::$system_conf->limits['max_registrations']; ?>" min="0" data-leave-validation="<?php echo FreshRSS_Context::$system_conf->limits['max_registrations']; ?>"/>
+				<?php echo _i('help'); ?> <?php echo _t('admin.user.registration.help'); ?>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<?php
+					$number = count(listUsers());
+					echo _t($number > 1 ? 'admin.user.numbers' : 'admin.user.number', $number);
+				?>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
+			</div>
+		</div>
+	</form>
+
 	<form method="post" action="<?php echo _url('user', 'create'); ?>">
 	<form method="post" action="<?php echo _url('user', 'create'); ?>">
 		<legend><?php echo _t('admin.user.create'); ?></legend>
 		<legend><?php echo _t('admin.user.create'); ?></legend>
 
 

+ 34 - 3
app/views/user/profile.phtml

@@ -18,11 +18,11 @@
 		</div>
 		</div>
 
 
 		<div class="form-group">
 		<div class="form-group">
-			<label class="group-name" for="passwordPlain"><?php echo _t('conf.profile.password_form'); ?></label>
+			<label class="group-name" for="newPasswordPlain"><?php echo _t('conf.profile.password_form'); ?></label>
 			<div class="group-controls">
 			<div class="group-controls">
 				<div class="stick">
 				<div class="stick">
-					<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>
+					<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<a class="btn toggle-password" data-toggle="newPasswordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				</div>
 				<?php echo _i('help'); ?> <?php echo _t('conf.profile.password_format'); ?>
 				<?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>
@@ -57,4 +57,35 @@
 			</div>
 			</div>
 		</div>
 		</div>
 	</form>
 	</form>
+
+	<?php if (!FreshRSS_Auth::hasAccess('admin')) { ?>
+	<form id="crypto-form" method="post" action="<?php echo _url('user', 'delete'); ?>">
+		<legend><?php echo _t('conf.profile.delete'); ?></legend>
+
+		<p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.attention'); ?></span> <?php echo _t('conf.profile.delete.warn'); ?></p>
+
+		<div class="form-group">
+			<label class="group-name" for="passwordPlain"><?php echo _t('gen.auth.password'); ?></label>
+			<div class="group-controls">
+					<input type="password" id="passwordPlain" required="required" />
+					<input type="hidden" id="challenge" name="challenge" /><br />
+					<noscript><strong><?php echo _t('gen.js.should_be_activated'); ?></strong></noscript>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<?php
+					$redirect_url = urlencode(Minz_Url::display(
+						array('c' => 'user', 'a' => 'profile'),
+						'php', true
+					));
+				?>
+				<input type="hidden" name="r" value="<?php echo $redirect_url; ?>" />
+				<input type="hidden" name="username" id="username" value="<?php echo Minz_Session::param('currentUser', '_'); ?>" />
+				<button type="submit" class="btn btn-attention confirm"><?php echo _t('gen.action.remove'); ?></button>
+			</div>
+		</div>
+	</form>
+	<?php } ?>
 </div>
 </div>

+ 2 - 1
constants.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-define('FRESHRSS_VERSION', '1.1.1-beta');
+define('FRESHRSS_VERSION', '1.1.2-dev');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 define('FRESHRSS_UPDATE_WEBSITE', 'https://update.freshrss.org?v=' . FRESHRSS_VERSION);
 define('FRESHRSS_UPDATE_WEBSITE', 'https://update.freshrss.org?v=' . FRESHRSS_VERSION);
 define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
 define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
@@ -19,6 +19,7 @@ define('FRESHRSS_PATH', dirname(__FILE__));
 		define('UPDATE_FILENAME', DATA_PATH . '/update.php');
 		define('UPDATE_FILENAME', DATA_PATH . '/update.php');
 		define('USERS_PATH', DATA_PATH . '/users');
 		define('USERS_PATH', DATA_PATH . '/users');
 		define('CACHE_PATH', DATA_PATH . '/cache');
 		define('CACHE_PATH', DATA_PATH . '/cache');
+		define('PSHB_PATH', DATA_PATH . '/PubSubHubbub');
 
 
 	define('LIB_PATH', FRESHRSS_PATH . '/lib');
 	define('LIB_PATH', FRESHRSS_PATH . '/lib');
 	define('APP_PATH', FRESHRSS_PATH . '/app');
 	define('APP_PATH', FRESHRSS_PATH . '/app');

+ 1 - 0
data/PubSubHubbub/feeds/.gitignore

@@ -0,0 +1 @@
+*/*

+ 7 - 0
data/PubSubHubbub/feeds/README.md

@@ -0,0 +1,7 @@
+List of canonical URLS of the various feeds users have subscribed to.
+Several users can have subscribed to the same feed.
+
+* ./base64url(canonicalUrl)/
+	* ./!hub.json
+	* ./user1.txt
+	* ./user2.txt

+ 1 - 0
data/PubSubHubbub/keys/.gitignore

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

+ 4 - 0
data/PubSubHubbub/keys/README.md

@@ -0,0 +1,4 @@
+List of keys given to PubSubHubbub hubs
+
+* ./sha1(random + salt).txt
+	* base64url(canonicalUrl)

+ 38 - 7
data/config.default.php

@@ -1,7 +1,7 @@
 <?php
 <?php
 
 
-# Do not modify this file, which is only a template.
-# See `config.php` after the install process is completed.
+# Do not modify this file, which defines default values,
+# but edit `config.php` instead, after the install process is completed.
 return array(
 return array(
 
 
 	# Set to `development` to get additional error messages,
 	# Set to `development` to get additional error messages,
@@ -11,9 +11,11 @@ return array(
 	# Used to make crypto more unique. Generated during install.
 	# Used to make crypto more unique. Generated during install.
 	'salt' => '',
 	'salt' => '',
 
 
-	# Leave empty for most cases.
-	# Ability to override the address of the FreshRSS instance,
-	# used when building absolute URLs.
+	# Specify address of the FreshRSS instance,
+	# used when building absolute URLs, e.g. for PubSubHubbub.
+	# Examples:
+	# https://example.net/FreshRSS/p/
+	# https://freshrss.example.net/
 	'base_url' => '',
 	'base_url' => '',
 
 
 	# Natural language of the user interface, e.g. `en`, `fr`.
 	# Natural language of the user interface, e.g. `en`, `fr`.
@@ -55,6 +57,10 @@ return array(
 	#	SimplePie, which is retrieving RSS feeds via HTTP requests.
 	#	SimplePie, which is retrieving RSS feeds via HTTP requests.
 	'simplepie_syslog_enabled' => true,
 	'simplepie_syslog_enabled' => true,
 
 
+	# Enable or not support of PubSubHubbub.
+	# /!\ It should NOT be enabled if base_url is not reachable by an external server.
+	'pubsubhubbub_enabled' => false,
+
 	'limits' => array(
 	'limits' => array(
 
 
 		# Duration in seconds of the SimplePie cache,
 		# Duration in seconds of the SimplePie cache,
@@ -75,6 +81,25 @@ return array(
 		# Max number of categories for a user.
 		# Max number of categories for a user.
 		'max_categories' => 16384,
 		'max_categories' => 16384,
 
 
+		# Max number of accounts that anonymous users can create
+		#   0 for an unlimited number of accounts
+		#   1 is to not allow user registrations (1 is corresponding to the admin account)
+		'max_registrations' => 1,
+	),
+
+	# Options used by cURL when making HTTP requests, e.g. when the SimplePie library retrieves feeds.
+	# http://php.net/manual/function.curl-setopt
+	'curl_options' => array(
+		# Options to disable SSL/TLS certificate check (e.g. for self-signed HTTPS)
+		//CURLOPT_SSL_VERIFYHOST => 0,
+		//CURLOPT_SSL_VERIFYPEER => false,
+
+		# Options to use a proxy for retrieving feeds.
+		//CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
+		//CURLOPT_PROXY => '127.0.0.1',
+		//CURLOPT_PROXYPORT => 8080,
+		//CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
+		//CURLOPT_PROXYUSERPWD => 'user:password',
 	),
 	),
 
 
 	'db' => array(
 	'db' => array(
@@ -83,7 +108,7 @@ return array(
 		'type' => 'sqlite',
 		'type' => 'sqlite',
 
 
 		# MySQL host.
 		# MySQL host.
-		'host' => '',
+		'host' => 'localhost',
 
 
 		# MySQL user.
 		# MySQL user.
 		'user' => '',
 		'user' => '',
@@ -95,7 +120,13 @@ return array(
 		'base' => '',
 		'base' => '',
 
 
 		# MySQL table prefix.
 		# MySQL table prefix.
-		'prefix' => '',
+		'prefix' => 'freshrss_',
+
+		'pdo_options' => array(
+			//PDO::MYSQL_ATTR_SSL_KEY	=> '/path/to/client-key.pem',
+			//PDO::MYSQL_ATTR_SSL_CERT	=> '/path/to/client-cert.pem',
+			//PDO::MYSQL_ATTR_SSL_CA	=> '/path/to/ca-cert.pem',
+		),
 
 
 	),
 	),
 
 

+ 1 - 1
data/users/_/config.default.php

@@ -25,7 +25,7 @@ return array (
 
 
 	# In the case an article has changed (e.g. updated content):
 	# In the case an article has changed (e.g. updated content):
 	#	Set to `true` to mark it unread, or `false` to leave it as-is.
 	#	Set to `true` to mark it unread, or `false` to leave it as-is.
-	'mark_updated_article_unread' => false,
+	'mark_updated_article_unread' => false, //TODO: -1 => ignore, 0 => update, 1 => update and mark as unread
 
 
 	'sort_order' => 'DESC',
 	'sort_order' => 'DESC',
 	'anon_access' => false,
 	'anon_access' => false,

+ 11 - 17
lib/Minz/Configuration.php

@@ -39,7 +39,7 @@ class Minz_Configuration {
 			throw new Minz_FileNotExistException($filename);
 			throw new Minz_FileNotExistException($filename);
 		}
 		}
 
 
-		$data = @include($filename);
+		$data = include($filename);
 		if (is_array($data)) {
 		if (is_array($data)) {
 			return $data;
 			return $data;
 		} else {
 		} else {
@@ -84,11 +84,6 @@ class Minz_Configuration {
 	 */
 	 */
 	private $data = array();
 	private $data = array();
 
 
-	/**
-	 * The default values, an empty array by default.
-	 */
-	private $data_default = array();
-
 	/**
 	/**
 	 * An object which help to set good values in configuration.
 	 * An object which help to set good values in configuration.
 	 */
 	 */
@@ -119,21 +114,22 @@ class Minz_Configuration {
 	                             $configuration_setter = null) {
 	                             $configuration_setter = null) {
 		$this->namespace = $namespace;
 		$this->namespace = $namespace;
 		$this->config_filename = $config_filename;
 		$this->config_filename = $config_filename;
+		$this->default_filename = $default_filename;
+		$this->_configurationSetter($configuration_setter);
+
+		if (!is_null($this->default_filename)) {
+			$this->data = self::load($this->default_filename);
+		}
 
 
 		try {
 		try {
-			$this->data = self::load($this->config_filename);
+			$this->data = array_replace_recursive(
+				$this->data, self::load($this->config_filename)
+			);
 		} catch (Minz_FileNotExistException $e) {
 		} catch (Minz_FileNotExistException $e) {
-			if (is_null($default_filename)) {
+			if (is_null($this->default_filename)) {
 				throw $e;
 				throw $e;
 			}
 			}
 		}
 		}
-
-		$this->default_filename = $default_filename;
-		if (!is_null($this->default_filename)) {
-			$this->data_default = self::load($this->default_filename);
-		}
-
-		$this->_configurationSetter($configuration_setter);
 	}
 	}
 
 
 	/**
 	/**
@@ -160,8 +156,6 @@ class Minz_Configuration {
 			return $this->data[$key];
 			return $this->data[$key];
 		} elseif (!is_null($default)) {
 		} elseif (!is_null($default)) {
 			return $default;
 			return $default;
-		} elseif (isset($this->data_default[$key])) {
-			return $this->data_default[$key];
 		} else {
 		} else {
 			Minz_Log::warning($key . ' does not exist in configuration');
 			Minz_Log::warning($key . ' does not exist in configuration');
 			return null;
 			return null;

+ 1 - 1
lib/Minz/Extension.php

@@ -168,7 +168,7 @@ class Minz_Extension {
 		$url = '/ext.php?f=' . $file_name_url .
 		$url = '/ext.php?f=' . $file_name_url .
 		       '&amp;t=' . $type .
 		       '&amp;t=' . $type .
 		       '&amp;' . $mtime;
 		       '&amp;' . $mtime;
-		return Minz_Url::display($url);
+		return Minz_Url::display($url, 'php');
 	}
 	}
 
 
 	/**
 	/**

+ 4 - 6
lib/Minz/ModelPdo.php

@@ -53,21 +53,19 @@ class Minz_ModelPdo {
 		$this->current_user = $currentUser;
 		$this->current_user = $currentUser;
 		self::$sharedCurrentUser = $currentUser;
 		self::$sharedCurrentUser = $currentUser;
 
 
+		$driver_options = isset($conf->db['pdo_options']) && is_array($conf->db['pdo_options']) ? $conf->db['pdo_options'] : array();
+
 		try {
 		try {
 			$type = $db['type'];
 			$type = $db['type'];
 			if ($type === 'mysql') {
 			if ($type === 'mysql') {
 				$string = 'mysql:host=' . $db['host']
 				$string = 'mysql:host=' . $db['host']
 				        . ';dbname=' . $db['base']
 				        . ';dbname=' . $db['base']
 				        . ';charset=utf8';
 				        . ';charset=utf8';
-				$driver_options = array(
-					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
-				);
+				$driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8';
 				$this->prefix = $db['prefix'] . $currentUser . '_';
 				$this->prefix = $db['prefix'] . $currentUser . '_';
 			} elseif ($type === 'sqlite') {
 			} elseif ($type === 'sqlite') {
 				$string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
 				$string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
-				$driver_options = array(
-					//PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
-				);
+				//$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
 				$this->prefix = '';
 				$this->prefix = '';
 			} else {
 			} else {
 				throw new Minz_PDOConnectionException(
 				throw new Minz_PDOConnectionException(

+ 28 - 17
lib/Minz/Request.php

@@ -85,26 +85,37 @@ class Minz_Request {
 	}
 	}
 
 
 	/**
 	/**
-	 * Détermine la base de l'url
-	 * @return la base de l'url
+	 * Try to guess the base URL from $_SERVER information
+	 *
+	 * @return the base url (e.g. http://example.com/)
 	 */
 	 */
-	public static function getBaseUrl($baseUrlSuffix = '') {
-		$conf = Minz_Configuration::get('system');
-		$url = $conf->base_url;
-		if ($url == '' || !preg_match('%^https?://%i', $url)) {
-			$url = 'http';
-			$host = empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST'];
-			$port = empty($_SERVER['SERVER_PORT']) ? 80 : $_SERVER['SERVER_PORT'];
-			if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
-				$url .= 's://' . $host . ($port == 443 ? '' : ':' . $port);
-			} else {
-				$url .= '://' . $host . ($port == 80 ? '' : ':' . $port);
-			}
-			$url .= isset($_SERVER['REQUEST_URI']) ? dirname($_SERVER['REQUEST_URI']) : '';
+	public static function guessBaseUrl() {
+		$url = 'http';
+		$host = empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST'];
+		$port = empty($_SERVER['SERVER_PORT']) ? 80 : $_SERVER['SERVER_PORT'];
+		if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
+			$url .= 's://' . $host . ($port == 443 ? '' : ':' . $port);
 		} else {
 		} else {
-			$url = rtrim($url, '/\\') . $baseUrlSuffix;
+			$url .= '://' . $host . ($port == 80 ? '' : ':' . $port);
 		}
 		}
-		return filter_var($url . '/', FILTER_SANITIZE_URL);
+		if (isset($_SERVER['REQUEST_URI'])) {
+			$path = $_SERVER['REQUEST_URI'];
+			$url .= substr($path, -1) === '/' ? substr($path, 0, -1) : dirname($path);
+		}
+
+		return filter_var($url, FILTER_SANITIZE_URL);
+	}
+
+	/**
+	 * Return the base_url from configuration and add a suffix if given.
+	 *
+	 * @param $base_url_suffix a string to add at base_url (default: empty string)
+	 * @return the base_url with a suffix.
+	 */
+	public static function getBaseUrl($base_url_suffix = '') {
+		$conf = Minz_Configuration::get('system');
+		$url = rtrim($conf->base_url, '/\\') . $base_url_suffix;
+		return filter_var($url, FILTER_SANITIZE_URL);
 	}
 	}
 
 
 	/**
 	/**

+ 6 - 1
lib/Minz/Url.php

@@ -25,14 +25,19 @@ class Minz_Url {
 
 
 		if ($absolute) {
 		if ($absolute) {
 			$url_string = Minz_Request::getBaseUrl(PUBLIC_TO_INDEX_PATH);
 			$url_string = Minz_Request::getBaseUrl(PUBLIC_TO_INDEX_PATH);
+			if ($url_string === PUBLIC_TO_INDEX_PATH) {
+				$url_string = Minz_Request::guessBaseUrl();
+			}
 		} else {
 		} else {
 			$url_string = $isArray ? '.' : PUBLIC_RELATIVE;
 			$url_string = $isArray ? '.' : PUBLIC_RELATIVE;
 		}
 		}
 
 
 		if ($isArray) {
 		if ($isArray) {
 			$url_string .= self::printUri($url, $encodage);
 			$url_string .= self::printUri($url, $encodage);
-		} else {
+		} elseif ($encodage === 'html') {
 			$url_string = Minz_Helper::htmlspecialchars_utf8($url_string . $url);
 			$url_string = Minz_Helper::htmlspecialchars_utf8($url_string . $url);
+		} else {
+			$url_string .= $url;
 		}
 		}
 
 
 		return $url_string;
 		return $url_string;

+ 1 - 0
lib/Minz/View.php

@@ -91,6 +91,7 @@ class Minz_View {
 	 * Construit le layout
 	 * Construit le layout
 	 */
 	 */
 	public function buildLayout () {
 	public function buildLayout () {
+		header('Content-Type: text/html; charset=UTF-8');
 		$this->includeFile(self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME);
 		$this->includeFile(self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME);
 	}
 	}
 
 

+ 78 - 71
lib/SimplePie/SimplePie.php

@@ -456,7 +456,7 @@ class SimplePie
 	 * @see SimplePie::subscribe_url()
 	 * @see SimplePie::subscribe_url()
 	 * @access private
 	 * @access private
 	 */
 	 */
-	public $permanent_url = null;	//FreshRSS
+	public $permanent_url = null;
 
 
 	/**
 	/**
 	 * @var object Instance of SimplePie_File to use as a feed
 	 * @var object Instance of SimplePie_File to use as a feed
@@ -479,6 +479,13 @@ class SimplePie
 	 */
 	 */
 	public $timeout = 10;
 	public $timeout = 10;
 
 
+	/**
+	 * @var array Custom curl options
+	 * @see SimplePie::set_curl_options()
+	 * @access private
+	 */
+	public $curl_options = array();
+
 	/**
 	/**
 	 * @var bool Forces fsockopen() to be used for remote files instead
 	 * @var bool Forces fsockopen() to be used for remote files instead
 	 * of cURL, even if a new enough version is installed
 	 * of cURL, even if a new enough version is installed
@@ -754,7 +761,7 @@ class SimplePie
 		else
 		else
 		{
 		{
 			$this->feed_url = $this->registry->call('Misc', 'fix_protocol', array($url, 1));
 			$this->feed_url = $this->registry->call('Misc', 'fix_protocol', array($url, 1));
-			$this->permanent_url = $this->feed_url;	//FreshRSS
+			$this->permanent_url = $this->feed_url;
 		}
 		}
 	}
 	}
 
 
@@ -769,7 +776,7 @@ class SimplePie
 		if ($file instanceof SimplePie_File)
 		if ($file instanceof SimplePie_File)
 		{
 		{
 			$this->feed_url = $file->url;
 			$this->feed_url = $file->url;
-			$this->permanent_url = $this->feed_url;	//FreshRSS
+			$this->permanent_url = $this->feed_url;
 			$this->file =& $file;
 			$this->file =& $file;
 			return true;
 			return true;
 		}
 		}
@@ -807,6 +814,19 @@ class SimplePie
 	{
 	{
 		$this->timeout = (int) $timeout;
 		$this->timeout = (int) $timeout;
 	}
 	}
+    
+	/**
+	 * Set custom curl options
+	 *
+	 * This allows you to change default curl options
+	 *
+	 * @since 1.0 Beta 3
+	 * @param array $curl_options Curl options to add to default settings
+	 */
+	public function set_curl_options(array $curl_options = array())
+	{
+		$this->curl_options = $curl_options;
+	}
 
 
 	/**
 	/**
 	 * Force SimplePie to use fsockopen() instead of cURL
 	 * Force SimplePie to use fsockopen() instead of cURL
@@ -1251,7 +1271,7 @@ class SimplePie
 		$this->enable_exceptions = $enable;
 		$this->enable_exceptions = $enable;
 	}
 	}
 
 
-	function cleanMd5($rss)	//FreshRSS
+	function cleanMd5($rss)
 	{
 	{
 		return md5(preg_replace(array('#<(lastBuildDate|pubDate|updated|feedDate|dc:date|slash:comments)>[^<]+</\\1>#', '#<!--.+?-->#s'), '', $rss));
 		return md5(preg_replace(array('#<(lastBuildDate|pubDate|updated|feedDate|dc:date|slash:comments)>[^<]+</\\1>#', '#<!--.+?-->#s'), '', $rss));
 	}
 	}
@@ -1297,7 +1317,7 @@ class SimplePie
 		// Pass whatever was set with config options over to the sanitizer.
 		// Pass whatever was set with config options over to the sanitizer.
 		// Pass the classes in for legacy support; new classes should use the registry instead
 		// Pass the classes in for legacy support; new classes should use the registry instead
 		$this->sanitize->pass_cache_data($this->cache, $this->cache_location, $this->cache_name_function, $this->registry->get_class('Cache'));
 		$this->sanitize->pass_cache_data($this->cache, $this->cache_location, $this->cache_name_function, $this->registry->get_class('Cache'));
-		$this->sanitize->pass_file_data($this->registry->get_class('File'), $this->timeout, $this->useragent, $this->force_fsockopen);
+		$this->sanitize->pass_file_data($this->registry->get_class('File'), $this->timeout, $this->useragent, $this->force_fsockopen, $this->curl_options);
 
 
 		if (!empty($this->multifeed_url))
 		if (!empty($this->multifeed_url))
 		{
 		{
@@ -1342,7 +1362,7 @@ class SimplePie
 			// Fetch the data via SimplePie_File into $this->raw_data
 			// Fetch the data via SimplePie_File into $this->raw_data
 			if (($fetched = $this->fetch_data($cache)) === true)
 			if (($fetched = $this->fetch_data($cache)) === true)
 			{
 			{
-				return $this->data['mtime'];	//FreshRSS
+				return $this->data['mtime'];
 			}
 			}
 			elseif ($fetched === false) {
 			elseif ($fetched === false) {
 				return false;
 				return false;
@@ -1350,7 +1370,7 @@ class SimplePie
 
 
 			list($headers, $sniffed) = $fetched;
 			list($headers, $sniffed) = $fetched;
 
 
-			if (isset($this->data['md5']))	//FreshRSS
+			if (isset($this->data['md5']))
 			{
 			{
 				$md5 = $this->data['md5'];
 				$md5 = $this->data['md5'];
 			}
 			}
@@ -1435,8 +1455,8 @@ class SimplePie
 						$this->data['headers'] = $headers;
 						$this->data['headers'] = $headers;
 					}
 					}
 					$this->data['build'] = SIMPLEPIE_BUILD;
 					$this->data['build'] = SIMPLEPIE_BUILD;
-					$this->data['mtime'] = time();	//FreshRSS
-					$this->data['md5'] = empty($md5) ? $this->cleanMd5($this->raw_data) : $md5;	//FreshRSS
+					$this->data['mtime'] = time();
+					$this->data['md5'] = empty($md5) ? $this->cleanMd5($this->raw_data) : $md5;
 
 
 					// Cache the file if caching is enabled
 					// Cache the file if caching is enabled
 					if ($cache && !$cache->save($this))
 					if ($cache && !$cache->save($this))
@@ -1451,7 +1471,7 @@ class SimplePie
 		if (isset($parser))
 		if (isset($parser))
 		{
 		{
 			// We have an error, just set SimplePie_Misc::error to it and quit
 			// We have an error, just set SimplePie_Misc::error to it and quit
-			$this->error = sprintf('This XML document is invalid, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column());
+			$this->error = sprintf('This XML document is invalid, likely due to invalid characters. XML error: %s at line %d, column %d, encoding %s, URL: %s', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column(), $encoding, $this->feed_url);
 		}
 		}
 		else
 		else
 		{
 		{
@@ -1477,7 +1497,7 @@ class SimplePie
 		{
 		{
 			// Load the Cache
 			// Load the Cache
 			$this->data = $cache->load();
 			$this->data = $cache->load();
-			if ($cache->mtime() + $this->cache_duration > time())	//FreshRSS
+			if ($cache->mtime() + $this->cache_duration > time())
 			{
 			{
 				$this->raw_data = false;
 				$this->raw_data = false;
 				return true;	// If the cache is still valid, just return true
 				return true;	// If the cache is still valid, just return true
@@ -1514,71 +1534,58 @@ class SimplePie
 					}
 					}
 				}
 				}
 				// Check if the cache has been updated
 				// Check if the cache has been updated
-				else //if ($cache->mtime() + $this->cache_duration < time())	//FreshRSS removed
+				else
 				{
 				{
-					// If we have last-modified and/or etag set
-					//if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag']))	//FreshRSS removed
+					$headers = array(
+						'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
+					);
+					if (isset($this->data['headers']['last-modified']))
 					{
 					{
-						$headers = array(
-							'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
-						);
-						if (isset($this->data['headers']['last-modified']))
-						{
-							$headers['if-modified-since'] = $this->data['headers']['last-modified'];
-						}
-						if (isset($this->data['headers']['etag']))
-						{
-							$headers['if-none-match'] = $this->data['headers']['etag'];
-						}
+						$headers['if-modified-since'] = $this->data['headers']['last-modified'];
+					}
+					if (isset($this->data['headers']['etag']))
+					{
+						$headers['if-none-match'] = $this->data['headers']['etag'];
+					}
 
 
-						$file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen));	//FreshRSS
+					$file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options));
 
 
-						if ($file->success)
+					if ($file->success)
+					{
+						if ($file->status_code === 304)
 						{
 						{
-							if ($file->status_code === 304)
-							{
-								$cache->touch();
-								return true;
-							}
+							$cache->touch();
+							return true;
 						}
 						}
-						else
+					}
+					else
+					{
+						$cache->touch();
+						$this->error = $file->error;
+						return !empty($this->data);
+					}
+
+					$md5 = $this->cleanMd5($file->body);
+					if ($this->data['md5'] === $md5) {
+						if ($this->syslog_enabled)
 						{
 						{
-							$cache->touch();	//FreshRSS
-							$this->error = $file->error;	//FreshRSS
-							return !empty($this->data);	//FreshRSS
-							//unset($file);	//FreshRSS removed
+							syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url));
 						}
 						}
-					}
-					{	//FreshRSS
-						$md5 = $this->cleanMd5($file->body);
-						if ($this->data['md5'] === $md5) {
-							if ($this->syslog_enabled)
-							{
-								syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url));
-							}
-							$cache->touch();
-							return true;	//Content unchanged even though server did not send a 304
-						} else {
-							if ($this->syslog_enabled)
-							{
-								syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url));
-							}
-							$this->data['md5'] = $md5;
+						$cache->touch();
+						return true;	//Content unchanged even though server did not send a 304
+					} else {
+						if ($this->syslog_enabled)
+						{
+							syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url));
 						}
 						}
+						$this->data['md5'] = $md5;
 					}
 					}
 				}
 				}
-				//// If the cache is still valid, just return true
-				//else	//FreshRSS removed
-				//{
-				//	$this->raw_data = false;
-				//	return true;
-				//}
-			}
-			// If the cache is empty, delete it
+			}
+			// If the cache is empty
 			else
 			else
 			{
 			{
-				//$cache->unlink();	//FreshRSS removed
-				$cache->touch();	//FreshRSS
+				$cache->touch();	//To keep the date/time of the last tentative update
 				$this->data = array();
 				$this->data = array();
 			}
 			}
 		}
 		}
@@ -1594,7 +1601,7 @@ class SimplePie
 				$headers = array(
 				$headers = array(
 					'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
 					'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
 				);
 				);
-				$file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen));
+				$file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options));
 			}
 			}
 		}
 		}
 		// If the file connection has an error, set SimplePie::error to that and quit
 		// If the file connection has an error, set SimplePie::error to that and quit
@@ -1611,15 +1618,15 @@ class SimplePie
 
 
 			if (!$locate->is_feed($file))
 			if (!$locate->is_feed($file))
 			{
 			{
-				$copyStatusCode = $file->status_code;	//FreshRSS
-				$copyContentType = $file->headers['content-type'];	//FreshRSS
+				$copyStatusCode = $file->status_code;
+				$copyContentType = $file->headers['content-type'];
 				// We need to unset this so that if SimplePie::set_file() has been called that object is untouched
 				// We need to unset this so that if SimplePie::set_file() has been called that object is untouched
 				unset($file);
 				unset($file);
 				try
 				try
 				{
 				{
 					if (!($file = $locate->find($this->autodiscovery, $this->all_discovered_feeds)))
 					if (!($file = $locate->find($this->autodiscovery, $this->all_discovered_feeds)))
 					{
 					{
-						$this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`";	//FreshRSS
+						$this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`";
 						$this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__));
 						$this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__));
 						return false;
 						return false;
 					}
 					}
@@ -1634,8 +1641,8 @@ class SimplePie
 				if ($cache)
 				if ($cache)
 				{
 				{
 					$this->data = array('url' => $this->feed_url, 'feed_url' => $file->url, 'build' => SIMPLEPIE_BUILD);
 					$this->data = array('url' => $this->feed_url, 'feed_url' => $file->url, 'build' => SIMPLEPIE_BUILD);
-					$this->data['mtime'] = time();	//FreshRSS
-					$this->data['md5'] = empty($md5) ? $this->cleanMd5($file->body) : $md5;	//FreshRSS
+					$this->data['mtime'] = time();
+					$this->data['md5'] = empty($md5) ? $this->cleanMd5($file->body) : $md5;
 					if (!$cache->save($this))
 					if (!$cache->save($this))
 					{
 					{
 						trigger_error("$this->cache_location is not writeable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
 						trigger_error("$this->cache_location is not writeable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
@@ -1648,7 +1655,7 @@ class SimplePie
 		}
 		}
 
 
 		$this->raw_data = $file->body;
 		$this->raw_data = $file->body;
-		$this->permanent_url = $file->permanent_url;	//FreshRSS
+		$this->permanent_url = $file->permanent_url;
 		$headers = $file->headers;
 		$headers = $file->headers;
 		$sniffer = $this->registry->create('Content_Type_Sniffer', array(&$file));
 		$sniffer = $this->registry->create('Content_Type_Sniffer', array(&$file));
 		$sniffed = $sniffer->get_type();
 		$sniffed = $sniffer->get_type();
@@ -1852,7 +1859,7 @@ class SimplePie
 	 */
 	 */
 	public function subscribe_url($permanent = false)
 	public function subscribe_url($permanent = false)
 	{
 	{
-		if ($permanent)	//FreshRSS
+		if ($permanent)
 		{
 		{
 			if ($this->permanent_url !== null)
 			if ($this->permanent_url !== null)
 			{
 			{

+ 2 - 10
lib/SimplePie/SimplePie/Cache/File.php

@@ -136,11 +136,7 @@ class SimplePie_Cache_File implements SimplePie_Cache_Base
 	 */
 	 */
 	public function mtime()
 	public function mtime()
 	{
 	{
-		//if (file_exists($this->name))	//FreshRSS removed
-		{
-			return @filemtime($this->name);	//FreshRSS
-		}
-		//return false;	//FreshRSS removed
+		return @filemtime($this->name);
 	}
 	}
 
 
 	/**
 	/**
@@ -150,11 +146,7 @@ class SimplePie_Cache_File implements SimplePie_Cache_Base
 	 */
 	 */
 	public function touch()
 	public function touch()
 	{
 	{
-		//if (file_exists($this->name))	//FreshRSS removed
-		{
-			return @touch($this->name);	//FreshRSS
-		}
-		//return false;	//FreshRSS removed
+		return @touch($this->name);
 	}
 	}
 
 
 	/**
 	/**

+ 0 - 1
lib/SimplePie/SimplePie/Decode/HTML/Entities.php

@@ -169,7 +169,6 @@ class SimplePie_Decode_HTML_Entities
 			case "\x09":
 			case "\x09":
 			case "\x0A":
 			case "\x0A":
 			case "\x0B":
 			case "\x0B":
-			case "\x0B":
 			case "\x0C":
 			case "\x0C":
 			case "\x20":
 			case "\x20":
 			case "\x3C":
 			case "\x3C":

+ 7 - 4
lib/SimplePie/SimplePie/File.php

@@ -66,7 +66,7 @@ class SimplePie_File
 	var $method = SIMPLEPIE_FILE_SOURCE_NONE;
 	var $method = SIMPLEPIE_FILE_SOURCE_NONE;
 	var $permanent_url;	//FreshRSS
 	var $permanent_url;	//FreshRSS
 
 
-	public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $syslog_enabled = SIMPLEPIE_SYSLOG)
+	public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $curl_options = array(), $syslog_enabled = SIMPLEPIE_SYSLOG)
 	{
 	{
 		if (class_exists('idna_convert'))
 		if (class_exists('idna_convert'))
 		{
 		{
@@ -75,7 +75,7 @@ class SimplePie_File
 			$url = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']);
 			$url = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']);
 		}
 		}
 		$this->url = $url;
 		$this->url = $url;
-		$this->permanent_url = $url;	//FreshRSS
+		$this->permanent_url = $url;
 		$this->useragent = $useragent;
 		$this->useragent = $useragent;
 		if (preg_match('/^http(s)?:\/\//i', $url))
 		if (preg_match('/^http(s)?:\/\//i', $url))
 		{
 		{
@@ -113,12 +113,15 @@ class SimplePie_File
 				curl_setopt($fp, CURLOPT_REFERER, $url);
 				curl_setopt($fp, CURLOPT_REFERER, $url);
 				curl_setopt($fp, CURLOPT_USERAGENT, $useragent);
 				curl_setopt($fp, CURLOPT_USERAGENT, $useragent);
 				curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2);
 				curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2);
-				curl_setopt($fp, CURLOPT_SSL_VERIFYPEER, false);	//FreshRSS
 				if (!ini_get('open_basedir') && !ini_get('safe_mode') && version_compare(SimplePie_Misc::get_curl_version(), '7.15.2', '>='))
 				if (!ini_get('open_basedir') && !ini_get('safe_mode') && version_compare(SimplePie_Misc::get_curl_version(), '7.15.2', '>='))
 				{
 				{
 					curl_setopt($fp, CURLOPT_FOLLOWLOCATION, 1);
 					curl_setopt($fp, CURLOPT_FOLLOWLOCATION, 1);
 					curl_setopt($fp, CURLOPT_MAXREDIRS, $redirects);
 					curl_setopt($fp, CURLOPT_MAXREDIRS, $redirects);
 				}
 				}
+				foreach ($curl_options as $curl_param => $curl_value)
+				{
+					curl_setopt($fp, $curl_param, $curl_value);
+				}
 
 
 				$this->headers = curl_exec($fp);
 				$this->headers = curl_exec($fp);
 				if (curl_errno($fp) === 23 || curl_errno($fp) === 61)
 				if (curl_errno($fp) === 23 || curl_errno($fp) === 61)
@@ -149,7 +152,7 @@ class SimplePie_File
 							$location = SimplePie_Misc::absolutize_url($this->headers['location'], $url);
 							$location = SimplePie_Misc::absolutize_url($this->headers['location'], $url);
 							$previousStatusCode = $this->status_code;
 							$previousStatusCode = $this->status_code;
 							$this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
 							$this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
-							$this->permanent_url = ($previousStatusCode == 301) ? $location : $url;	//FreshRSS
+							$this->permanent_url = ($previousStatusCode == 301) ? $location : $url;
 							return;
 							return;
 						}
 						}
 					}
 					}

+ 34 - 8
lib/SimplePie/SimplePie/Item.php

@@ -406,6 +406,30 @@ class SimplePie_Item
 			return null;
 			return null;
 		}
 		}
 	}
 	}
+	
+	/**
+	 * Get the media:thumbnail of the item
+	 *
+	 * Uses `<media:thumbnail>`
+	 *
+	 * 
+	 * @return array|null
+	 */
+	public function get_thumbnail()
+	{
+		if (!isset($this->data['thumbnail']))
+		{
+			if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'thumbnail'))
+			{
+				$this->data['thumbnail'] = $return[0]['attribs'][''];
+			}
+			else
+			{
+				$this->data['thumbnail'] = null;
+			}
+		}
+		return $this->data['thumbnail'];
+	}	
 
 
 	/**
 	/**
 	 * Get a category for the item
 	 * Get a category for the item
@@ -738,31 +762,31 @@ class SimplePie_Item
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
-			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated'))
+			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'pubDate'))
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
-			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'issued'))
+			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'date'))
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
-			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'created'))
+			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'date'))
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
-			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'modified'))
+			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated'))
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
-			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'pubDate'))
+			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'issued'))
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
-			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'date'))
+			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'created'))
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
-			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'date'))
+			elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'modified'))
 			{
 			{
 				$this->data['date']['raw'] = $return[0]['data'];
 				$this->data['date']['raw'] = $return[0]['data'];
 			}
 			}
@@ -2733,7 +2757,9 @@ class SimplePie_Item
 						{
 						{
 							foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail)
 							foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail)
 							{
 							{
-								$thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI);
+								if (isset($thumbnail['attribs']['']['url'])) {
+									$thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI);
+								}
 							}
 							}
 							if (is_array($thumbnails))
 							if (is_array($thumbnails))
 							{
 							{

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

@@ -148,7 +148,7 @@ class SimplePie_Locator
 		{
 		{
 			$sniffer = $this->registry->create('Content_Type_Sniffer', array($file));
 			$sniffer = $this->registry->create('Content_Type_Sniffer', array($file));
 			$sniffed = $sniffer->get_type();
 			$sniffed = $sniffer->get_type();
-			if (in_array($sniffed, array('application/rss+xml', 'application/rdf+xml', 'text/rdf', 'application/atom+xml', 'text/xml', 'application/xml', 'application/x-rss+xml')))	//FreshRSS
+			if (in_array($sniffed, array('application/rss+xml', 'application/rdf+xml', 'text/rdf', 'application/atom+xml', 'text/xml', 'application/xml', 'application/x-rss+xml')))
 			{
 			{
 				return true;
 				return true;
 			}
 			}

+ 4 - 4
lib/SimplePie/SimplePie/Misc.php

@@ -79,8 +79,8 @@ class SimplePie_Misc
 
 
 	public static function absolutize_url($relative, $base)
 	public static function absolutize_url($relative, $base)
 	{
 	{
-		if (substr($relative, 0, 2) === '//')	//FreshRSS: disable absolutize_url for "//www.example.net" which will pick HTTP or HTTPS automatically
-		{
+		if (substr($relative, 0, 2) === '//')
+		{//Allow protocol-relative URLs "//www.example.net" which will pick HTTP or HTTPS automatically
 			return $relative;
 			return $relative;
 		}
 		}
 		$iri = SimplePie_IRI::absolutize(new SimplePie_IRI($base), $relative);
 		$iri = SimplePie_IRI::absolutize(new SimplePie_IRI($base), $relative);
@@ -128,7 +128,7 @@ class SimplePie_Misc
 						{
 						{
 							$attribs[$j][2] = $attribs[$j][1];
 							$attribs[$j][2] = $attribs[$j][1];
 						}
 						}
-						$return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j]), 'UTF-8');	//FreshRSS
+						$return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j]), 'UTF-8');
 					}
 					}
 				}
 				}
 			}
 			}
@@ -142,7 +142,7 @@ class SimplePie_Misc
 		foreach ($element['attribs'] as $key => $value)
 		foreach ($element['attribs'] as $key => $value)
 		{
 		{
 			$key = strtolower($key);
 			$key = strtolower($key);
-			$full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"';	//FreshRSS
+			$full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"';
 		}
 		}
 		if ($element['self_closing'])
 		if ($element['self_closing'])
 		{
 		{

+ 3 - 3
lib/SimplePie/SimplePie/Parse/Date.php

@@ -173,7 +173,7 @@ class SimplePie_Parse_Date
 		'aug' => 8,
 		'aug' => 8,
 		'august' => 8,
 		'august' => 8,
 		'sep' => 9,
 		'sep' => 9,
-		'september' => 8,
+		'september' => 9,
 		'oct' => 10,
 		'oct' => 10,
 		'october' => 10,
 		'october' => 10,
 		'nov' => 11,
 		'nov' => 11,
@@ -331,8 +331,8 @@ class SimplePie_Parse_Date
 		'CCT' => 23400,
 		'CCT' => 23400,
 		'CDT' => -18000,
 		'CDT' => -18000,
 		'CEDT' => 7200,
 		'CEDT' => 7200,
-		'CEST' => 7200,	//FreshRSS
 		'CET' => 3600,
 		'CET' => 3600,
+		'CEST' => 7200,
 		'CGST' => -7200,
 		'CGST' => -7200,
 		'CGT' => -10800,
 		'CGT' => -10800,
 		'CHADT' => 49500,
 		'CHADT' => 49500,
@@ -721,7 +721,7 @@ class SimplePie_Parse_Date
 		{
 		{
 			$output .= substr($string, $position, $pos - $position);
 			$output .= substr($string, $position, $pos - $position);
 			$position = $pos + 1;
 			$position = $pos + 1;
-			if ($string[$pos - 1] !== '\\')
+			if ($pos === 0 || $string[$pos - 1] !== '\\')
 			{
 			{
 				$depth++;
 				$depth++;
 				while ($depth && $position < $length)
 				while ($depth && $position < $length)

+ 2 - 2
lib/SimplePie/SimplePie/Registry.php

@@ -113,7 +113,7 @@ class SimplePie_Registry
 	 */
 	 */
 	public function register($type, $class, $legacy = false)
 	public function register($type, $class, $legacy = false)
 	{
 	{
-		if (!is_subclass_of($class, $this->default[$type]))
+		if (!@is_subclass_of($class, $this->default[$type]))
 		{
 		{
 			return false;
 			return false;
 		}
 		}
@@ -222,4 +222,4 @@ class SimplePie_Registry
 		$result = call_user_func_array(array($class, $method), $parameters);
 		$result = call_user_func_array(array($class, $method), $parameters);
 		return $result;
 		return $result;
 	}
 	}
-}
+}

+ 2 - 2
lib/SimplePie/SimplePie/Sanitize.php

@@ -249,7 +249,7 @@ class SimplePie_Sanitize
 		{
 		{
 			if ($type & SIMPLEPIE_CONSTRUCT_MAYBE_HTML)
 			if ($type & SIMPLEPIE_CONSTRUCT_MAYBE_HTML)
 			{
 			{
-				$data = htmlspecialchars_decode($data, ENT_QUOTES);	//FreshRSS 
+				$data = htmlspecialchars_decode($data, ENT_QUOTES);
 				if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>)/', $data))
 				if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>)/', $data))
 				{
 				{
 					$type |= SIMPLEPIE_CONSTRUCT_HTML;
 					$type |= SIMPLEPIE_CONSTRUCT_HTML;
@@ -280,7 +280,7 @@ class SimplePie_Sanitize
 				$document->loadHTML($data);
 				$document->loadHTML($data);
 				restore_error_handler();
 				restore_error_handler();
 
 
-				$xpath = new DOMXPath($document);	//FreshRSS
+				$xpath = new DOMXPath($document);
 
 
 				// Strip comments
 				// Strip comments
 				if ($this->strip_comments)
 				if ($this->strip_comments)

+ 68 - 5
lib/lib_rss.php

@@ -83,6 +83,33 @@ function checkUrl($url) {
 	}
 	}
 }
 }
 
 
+
+/**
+ * Test if a given server address is publicly accessible.
+ *
+ * Note: for the moment it tests only if address is corresponding to a
+ * localhost address.
+ *
+ * @param $address the address to test, can be an IP or a URL.
+ * @return true if server is accessible, false else.
+ * @todo improve test with a more valid technique (e.g. test with an external server?)
+ */
+function server_is_public($address) {
+	$host = parse_url($address, PHP_URL_HOST);
+
+	$is_public = !in_array($host, array(
+		'127.0.0.1',
+		'localhost',
+		'localhost.localdomain',
+		'[::1]',
+		'localhost6',
+		'localhost6.localdomain6',
+	));
+
+	return $is_public;
+}
+
+
 function format_number($n, $precision = 0) {
 function format_number($n, $precision = 0) {
 	// number_format does not seem to be Unicode-compatible
 	// number_format does not seem to be Unicode-compatible
 	return str_replace(' ', ' ',  //Espace fine insécable
 	return str_replace(' ', ' ',  //Espace fine insécable
@@ -143,6 +170,7 @@ function customSimplePie() {
 	$simplePie->set_cache_location(CACHE_PATH);
 	$simplePie->set_cache_location(CACHE_PATH);
 	$simplePie->set_cache_duration($limits['cache_duration']);
 	$simplePie->set_cache_duration($limits['cache_duration']);
 	$simplePie->set_timeout($limits['timeout']);
 	$simplePie->set_timeout($limits['timeout']);
+	$simplePie->set_curl_options($system_conf->curl_options);
 	$simplePie->strip_htmltags(array(
 	$simplePie->strip_htmltags(array(
 		'base', 'blink', 'body', 'doctype', 'embed',
 		'base', 'blink', 'body', 'doctype', 'embed',
 		'font', 'form', 'frame', 'frameset', 'html',
 		'font', 'form', 'frame', 'frameset', 'html',
@@ -195,17 +223,27 @@ function sanitizeHTML($data, $base = '') {
 
 
 /* permet de récupérer le contenu d'un article pour un flux qui n'est pas complet */
 /* permet de récupérer le contenu d'un article pour un flux qui n'est pas complet */
 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');
 
 
 	Minz_Log::notice('FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
 	Minz_Log::notice('FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
-	$html = file_get_contents ($url);
+	$html = file_get_contents($url);
 
 
 	if ($html) {
 	if ($html) {
-		$doc = phpQuery::newDocument ($html);
-		$content = $doc->find ($path);
+		$doc = phpQuery::newDocument($html);
+		$content = $doc->find($path);
+
+		foreach (pq('img[data-src]') as $img) {
+			$imgP = pq($img);
+			$dataSrc = $imgP->attr('data-src');
+			if (strlen($dataSrc) > 4) {
+				$imgP->attr('src', $dataSrc);
+				$imgP->removeAttr('data-src');
+			}
+		}
+
 		return sanitizeHTML($content->__toString(), $url);
 		return sanitizeHTML($content->__toString(), $url);
 	} else {
 	} else {
-		throw new Exception ();
+		throw new Exception();
 	}
 	}
 }
 }
 
 
@@ -255,6 +293,22 @@ function listUsers() {
 }
 }
 
 
 
 
+/**
+ * Return if the maximum number of registrations has been reached.
+ *
+ * Note a max_regstrations of 0 means there is no limit.
+ *
+ * @return true if number of users >= max registrations, false else.
+ */
+function max_registrations_reached() {
+	$system_conf = Minz_Configuration::get('system');
+	$limit_registrations = $system_conf->limits['max_registrations'];
+	$number_accounts = count(listUsers());
+
+	return $limit_registrations > 0 && $number_accounts >= $limit_registrations;
+}
+
+
 /**
 /**
  * Register and return the configuration for a given user.
  * Register and return the configuration for a given user.
  *
  *
@@ -446,3 +500,12 @@ 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));
 }
 }
+
+//RFC 4648
+function base64url_encode($data) {
+	return strtr(rtrim(base64_encode($data), '='), '+/', '-_');
+}
+//RFC 4648
+function base64url_decode($data) {
+	return base64_decode(strtr($data, '-_', '+/'));
+}

+ 133 - 0
p/api/pshb.php

@@ -0,0 +1,133 @@
+<?php
+require('../../constants.php');
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
+
+define('MAX_PAYLOAD', 3145728);
+
+header('Content-Type: text/plain; charset=UTF-8');
+
+function logMe($text) {
+	file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+}
+
+$ORIGINAL_INPUT = file_get_contents('php://input', false, null, -1, MAX_PAYLOAD);
+
+//logMe(print_r(array('_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true));
+
+$key = isset($_GET['k']) ? substr($_GET['k'], 0, 128) : '';
+if (!ctype_xdigit($key)) {
+	header('HTTP/1.1 422 Unprocessable Entity');
+	die('Invalid feed key format!');
+}
+chdir(PSHB_PATH);
+$canonical64 = @file_get_contents('keys/' . $key . '.txt');
+if ($canonical64 === false) {
+	header('HTTP/1.1 404 Not Found');
+	logMe('Error: Feed key not found!: ' . $key);
+	die('Feed key not found!');
+}
+$canonical64 = trim($canonical64);
+if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) {
+	header('HTTP/1.1 500 Internal Server Error');
+	logMe('Error: Invalid key reference!: ' . $canonical64);
+	die('Invalid key reference!');
+}
+$hubFile = @file_get_contents('feeds/' . $canonical64 . '/!hub.json');
+if ($hubFile === false) {
+	header('HTTP/1.1 404 Not Found');
+	//@unlink('keys/' . $key . '.txt');
+	logMe('Error: Feed info not found!: ' . $canonical64);
+	die('Feed info not found!');
+}
+$hubJson = json_decode($hubFile, true);
+if (!$hubJson || empty($hubJson['key']) || $hubJson['key'] !== $key) {
+	header('HTTP/1.1 500 Internal Server Error');
+	logMe('Error: Invalid key cross-check!: ' . $key);
+	die('Invalid key cross-check!');
+}
+chdir('feeds/' . $canonical64);
+$users = glob('*.txt', GLOB_NOSORT);
+if (empty($users)) {
+	header('HTTP/1.1 410 Gone');
+	logMe('Error: Nobody is subscribed to this feed anymore!: ' . $canonical64);
+	die('Nobody is subscribed to this feed anymore!');
+}
+
+if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') {
+	$leaseSeconds = empty($_REQUEST['hub_lease_seconds']) ? 0 : intval($_REQUEST['hub_lease_seconds']);
+	if ($leaseSeconds > 60) {
+		$hubJson['lease_end'] = time() + $leaseSeconds;
+	} else {
+		unset($hubJson['lease_end']);
+	}
+	$hubJson['lease_start'] = time();
+	if (!isset($hubJson['error'])) {
+		$hubJson['error'] = true;	//Do not assume that PubSubHubbub works until the first successul push
+	}
+	file_put_contents('./!hub.json', json_encode($hubJson));
+	exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : '');
+}
+
+if ($ORIGINAL_INPUT == '') {
+	header('HTTP/1.1 422 Unprocessable Entity');
+	die('Missing XML payload!');
+}
+
+Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php');
+$system_conf = Minz_Configuration::get('system');
+$system_conf->auth_type = 'none';	// avoid necessity to be logged in (not saved!)
+Minz_Translate::init('en');
+Minz_Request::_param('ajax', true);
+$feedController = new FreshRSS_feed_Controller();
+
+$simplePie = customSimplePie();
+$simplePie->set_raw_data($ORIGINAL_INPUT);
+$simplePie->init();
+unset($ORIGINAL_INPUT);
+
+$links = $simplePie->get_links('self');
+$self = isset($links[0]) ? $links[0] : null;
+
+if ($self !== base64url_decode($canonical64)) {
+	//header('HTTP/1.1 422 Unprocessable Entity');
+	logMe('Warning: Self URL [' . $self . '] does not match registered canonical URL!: ' . base64url_decode($canonical64));
+	//die('Self URL does not match registered canonical URL!');
+	$self = base64url_decode($canonical64);
+}
+Minz_Request::_param('url', $self);
+
+$nb = 0;
+foreach ($users as $userFilename) {
+	$username = basename($userFilename, '.txt');
+	if (!file_exists(USERS_PATH . '/' . $username . '/config.php')) {
+		break;
+	}
+
+	try {
+		Minz_Session::_param('currentUser', $username);
+		Minz_Configuration::register('user',
+		                             join_path(USERS_PATH, $username, 'config.php'),
+		                             join_path(USERS_PATH, '_', 'config.default.php'));
+		FreshRSS_Context::init();
+		if ($feedController->actualizeAction($simplePie) > 0) {
+			$nb++;
+		}
+	} catch (Exception $e) {
+		logMe('Error: ' . $e->getMessage());
+	}
+}
+
+$simplePie->__destruct();
+unset($simplePie);
+
+if ($nb === 0) {
+	header('HTTP/1.1 410 Gone');
+	logMe('Error: Nobody is subscribed to this feed anymore after all!: ' . $self);
+	die('Nobody is subscribed to this feed anymore after all!');
+} elseif (!empty($hubJson['error'])) {
+	$hubJson['error'] = false;
+	file_put_contents('./!hub.json', json_encode($hubJson));
+}
+
+logMe('PubSubHubbub ' . $self . ' done: ' . $nb);
+exit('Done: ' . $nb . "\n");