Przeglądaj źródła

Merge branch 'dev' into beta

Marien Fressinaud 10 lat temu
rodzic
commit
5fbbfdea8a
76 zmienionych plików z 1233 dodań i 282 usunięć
  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
 
+## 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)
 
 * 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 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
 * Démo : http://demo.freshrss.org/
@@ -32,7 +33,7 @@ Nous sommes une communauté amicale.
 * Serveur modeste, par exemple sous Linux ou Windows
 	* Fonctionne même sur un Raspberry Pi avec des temps de réponse < 1s (testé sur 150 flux, 22k articles, soit 32Mo de données partiellement compressées)
 * Serveur Web Apache2 (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)
 	* 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+

+ 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 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
 * Demo: http://demo.freshrss.org/
@@ -32,7 +33,7 @@ We are a friendly community.
 * 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)
 * 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)
 	* 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+

+ 12 - 1
app/Controllers/authController.php

@@ -253,7 +253,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 				FreshRSS_Auth::giveAccess();
 				invalidateHttpCache();
 			} else {
-				Minz_Log::error($reason);
+				Minz_Log::warning($reason);
 
 				$res = array();
 				$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 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 = '';
-			if ($user != '' || $pass != '') {
+			if ($user != '' && $pass != '') {	//TODO: Sanitize
 				$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.
 			$feed->_id($id);
 			$feed->faviconPrepare();
+			//$feed->pubSubHubbubPrepare();	//TODO: prepare PubSubHubbub already when adding the feed
 
 			$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.
 	 *
 	 * Parameters are:
-	 *   - id (default: false)
+	 *   - id (default: false): Feed ID
+	 *   - url (default: false): Feed URL
 	 *   - 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.
 	 */
-	public function actualizeAction() {
+	public function actualizeAction($simplePiePush = null) {
 		@set_time_limit(300);
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -274,14 +276,15 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 		Minz_Session::_param('actualize_feeds', false);
 		$id = Minz_Request::param('id');
+		$url = Minz_Request::param('url');
 		$force = Minz_Request::param('force');
 
 		// Create a list of feeds to actualize.
 		// If id is set and valid, corresponding feed is added to the list but
 		// alone in order to automatize further process.
 		$feeds = array();
-		if ($id) {
-			$feed = $feedDAO->searchById($id);
+		if ($id || $url) {
+			$feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url);
 			if ($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);
 		$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;
 		$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 		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()) {
 				Minz_Log::notice('Feed already being actualized: ' . $feed->url());
 				continue;
 			}
 
-			$url = $feed->url();	//For detection of HTTP 301
 			try {
-				// Load entries
-				$feed->load(false);
+				if ($simplePiePush) {
+					$feed->loadEntries($simplePiePush);	//Used by PubSubHubbub
+				} else {
+					$feed->load(false);
+				}
 			} catch (FreshRSS_Feed_Exception $e) {
-				Minz_Log::notice($e->getMessage());
+				Minz_Log::warning($e->getMessage());
 				$feedDAO->updateLastUpdate($feed->id(), true);
 				$feed->unlock();
 				continue;
@@ -368,6 +387,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 							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()) {
 							$entryDAO->beginTransaction();
 						}
@@ -398,13 +425,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$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());
 				$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
 			}
 
 			$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();
 			$updated_feeds++;
 			unset($feed);
@@ -427,20 +473,20 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			Minz_Session::_param('notification', $notif);
 			// No layout in ajax request.
 			$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 {
-			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'];
 
 		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'),
 			                  array('c' => 'importExport', 'a' => 'index'));
 		}
@@ -69,7 +69,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 			if (!is_resource($zip)) {
 				// 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'),
 				                  array('c' => 'importExport', 'a' => 'index'));
 			}
@@ -77,7 +77,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			while (($zipfile = zip_read($zip)) !== false) {
 				if (!is_resource($zipfile)) {
 					// 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 {
 					$type_zipfile = $this->guessFileType(zip_entry_name($zipfile));
 					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() . ' · ');
 
 		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 = '';
-			if ($user != '' || $pass != '') {
+			if ($user != '' && $pass != '') {	//TODO: Sanitize
 				$httpAuth = $user . ':' . $pass;
 			}
 

+ 1 - 1
app/Controllers/updateController.php

@@ -63,7 +63,7 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 		curl_close($c);
 
 		if ($c_status !== 200) {
-			Minz_Log::error(
+			Minz_Log::warning(
 				'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
 	 * the common boiler plate for every action. It is triggered by the
 	 * underlying framework.
+	 *
+	 * @todo clean up the access condition.
 	 */
 	public function firstAction() {
-		if (!FreshRSS_Auth::hasAccess()) {
+		if (!FreshRSS_Auth::hasAccess() && !(
+				Minz_Request::actionName() === 'create' &&
+				!max_registrations_reached()
+		)) {
 			Minz_Error::error(403);
 		}
 	}
@@ -25,13 +30,17 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	public function profileAction() {
 		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()) {
 			$ok = true;
 
-			$passwordPlain = Minz_Request::param('passwordPlain', '', true);
+			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			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')) {
 					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 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() {
-		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;
 			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_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() {
-		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;
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
-			$username = Minz_Request::param('username');
 			$ok = ctype_alnum($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;
 				$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) {
 				$ok &= is_dir($user_data);
 			}
@@ -200,6 +257,10 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				$ok &= recursive_unlink($user_data);
 				//TODO: delete Persona file
 			}
+			if ($ok && $self_deletion) {
+				FreshRSS_Auth::removeAccess();
+				$redirect_url = array('c' => 'index', 'a' => 'index');
+			}
 			invalidateHttpCache();
 
 			$notif = array(
@@ -209,6 +270,30 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			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);
 	}
 }

+ 1 - 1
app/Models/CategoryDAO.php

@@ -13,7 +13,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			return $this->bd->lastInsertId();
 		} else {
 			$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;
 		}
 	}

+ 6 - 2
app/Models/ConfigurationSetter.php

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

+ 12 - 5
app/Models/EntryDAO.php

@@ -6,6 +6,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return parent::$sharedDbType !== 'sqlite';
 	}
 
+	public function hasNativeHex() {
+		return parent::$sharedDbType !== 'sqlite';
+	}
+
 	protected function addColumn($name) {
 		Minz_Log::debug('FreshRSS_EntryDAO::autoAddColumn: ' . $name);
 		$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) '
 			     . 'VALUES(?, ?, ?, ?, '
 			     . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
-			     . ', ?, ?, ?, ?, ?, ?, ?, ?)';
+			     . ', ?, ?, ?, '
+			     . ($this->hasNativeHex() ? 'X?' : '?')
+			     . ', ?, ?, ?, ?)';
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 		}
 
@@ -77,7 +83,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			substr($valuesTmp['link'], 0, 1023),
 			$valuesTmp['date'],
 			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_favorite'] ? 1 : 0,
 			$valuesTmp['id_feed'],
@@ -109,8 +115,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$sql = 'UPDATE `' . $this->prefix . 'entry` '
 			     . 'SET title=?, author=?, '
 			     . ($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=? '
 			     . 'WHERE id_feed=? AND guid=?';
 			$this->updateEntryPrepared = $this->bd->prepare($sql);
@@ -123,7 +130,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			substr($valuesTmp['link'], 0, 1023),
 			$valuesTmp['date'],
 			time(),
-			hex2bin($valuesTmp['hash']),
+			$this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),
 		);
 		if ($valuesTmp['is_read'] !== null) {
 			$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 $hash = null;
 	private $lockPath = '';
+	private $hubUrl = '';
+	private $selfUrl = '';
 
 	public function __construct($url, $validate=true) {
 		if ($validate) {
@@ -49,6 +51,12 @@ class FreshRSS_Feed extends Minz_Model {
 	public function url() {
 		return $this->url;
 	}
+	public function selfUrl() {
+		return $this->selfUrl;
+	}
+	public function hubUrl() {
+		return $this->hubUrl;
+	}
 	public function category() {
 		return $this->category;
 	}
@@ -96,6 +104,16 @@ class FreshRSS_Feed extends Minz_Model {
 	public function 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() {
 		if ($this->nbEntries < 0) {
 			$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -226,6 +244,11 @@ class FreshRSS_Feed extends Minz_Model {
 					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) {
 					// si on a utilisé l'auto-discover, notre url va avoir changé
 					$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();
 
 		foreach ($feed->get_items() as $item) {
@@ -333,4 +356,129 @@ class FreshRSS_Feed extends Minz_Model {
 	function unlock() {
 		@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;
 
 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');

+ 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
 
 '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');

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

@@ -160,8 +160,15 @@ return array(
 		'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>',
 		'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_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ů',
 		'user_list' => 'Seznam uživatelů',
 		'username' => 'Přihlašovací jméno',

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

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => '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>',
 		'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>',
@@ -84,6 +88,7 @@ return array(
 		'articles_per_page' => 'Počet článků na stranu',
 		'auto_load_more' => 'Načítat další články dole na stránce',
 		'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é”',
 		'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é',

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

@@ -102,6 +102,7 @@ return array(
 			'_' => 'Uživatel %s byl smazán',
 			'error' => 'Uživatele %s nelze smazat',
 		),
+		'set_registration' => 'Maximální počet účtů byl změněn',
 	),
 	'profile' => array(
 		'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',
 	),
 	'auth' => array(
+		'email' => 'Email',
 		'keep_logged_in' => 'Zapamatovat přihlášení <small>(1 měsíc)</small>',
 		'login' => 'Login',
 		'login_persona' => 'Přihlášení pomocí Persona',
 		'login_persona_problem' => 'Problém s připojením k Persona?',
 		'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í',
-		'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.',
 	),
 	'date' => array(

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

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 		'finish' => 'Dokončit instalaci',
 		'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',
+		'reinstall' => 'Reinstalovat FreshRSS',
 	),
 	'auth' => array(
 		'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(
 		'_' => 'Kontrola',
+		'already_installed' => 'Zjistili jsme, že FreshRSS je již nainstalován!',
 		'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',
 			'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ž',
 	'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',
+	'js' => array(
+		'confirm_reinstall' => 'Reinstalací FreshRSS ztratíte předchozí konfiguraci. Opravdu chcete pokračovat?',
+	),
 	'language' => array(
 		'_' => 'Jazyk',
 		'choose' => 'Vyberte jazyk FreshRSS',

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

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

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

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Neuen Benutzer erstellen',
 		'email_persona' => 'Anmelde-E-Mail-Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Sprache',
+		'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_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',
 		'user_list' => 'Liste der Benutzer',
 		'username' => 'Nutzername',

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

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => '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>',
 		'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>',

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

@@ -102,6 +102,7 @@ return array(
 			'_' => 'Der Benutzer %s ist gelöscht worden',
 			'error' => 'Der Benutzer %s kann nicht gelöscht werden',
 		),
+		'set_registration' => 'Die maximale Anzahl von Accounts wurde aktualisiert.',
 	),
 	'profile' => array(
 		'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',
 	),
 	'auth' => array(
+		'email' => 'E-Mail-Adresse',
 		'keep_logged_in' => 'Eingeloggt bleiben <small>(1 Monat)</small>',
 		'login' => 'Anmelden',
 		'login_persona' => 'Anmelden mit Persona',
 		'login_persona_problem' => 'Verbindungsproblem mit Persona?',
 		'logout' => 'Abmelden',
-		'password' => 'Passwort',
+		'password' => array(
+			'_' => 'Passwort',
+			'format' => '<small>mindestens 7 Zeichen</small>',
+		),
+		'registration' => array(
+			'_' => 'Neuer Account',
+			'ask' => 'Erstelle einen Account?',
+			'title' => 'Accounterstellung',
+		),
 		'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.',
 	),
 	'date' => array(

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

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 		'finish' => 'Installation fertigstellen',
 		'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',
+		'reinstall' => 'Neuinstallation von FreshRSS',
 	),
 	'auth' => array(
 		'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(
 		'_' => 'Überprüfungen',
+		'already_installed' => 'Wir haben festgestellt, dass FreshRSS bereits installiert wurde!',
 		'cache' => array(
 			'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.',
@@ -93,6 +96,9 @@ return array(
 	'delete_articles_after' => 'Entferne Artikel nach',
 	'fix_errors_before' => 'Bitte den Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.',
 	'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(
 		'_' => 'Sprache',
 		'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',
 		'validator' => 'Überprüfen Sie die Gültigkeit des Feeds',
 		'website' => 'Webseiten-URL',
+		'pubsubhubbub' => 'Sofortbenachrichtigung mit PubSubHubbub',
 	),
 	'import_export' => array(
 		'export' => 'Exportieren',

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

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Create new user',
 		'email_persona' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'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_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',
 		'user_list' => 'List of users',
 		'username' => 'Username',

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

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => '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>',
 		'password_api' => 'Password API<br /><small>(e.g., for mobile apps)</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',
 			'error' => 'User %s cannot be deleted',
 		),
+		'set_registration' => 'The maximum amount of accounts has been updated.',
 	),
 	'profile' => array(
 		'error' => 'Your profile cannot be modified',

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

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Delete all articles',
 	),
 	'auth' => array(
+		'email' => 'Email address',
 		'keep_logged_in' => 'Keep me logged in <small>(1 month)</small>',
 		'login' => 'Login',
 		'login_persona' => 'Login with Persona',
 		'login_persona_problem' => 'Connection problem with Persona?',
 		'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',
-		'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.',
 	),
 	'date' => array(

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

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 		'finish' => 'Complete installation',
 		'fix_errors_before' => 'Please fix errors before skipping to the next step.',
+		'keep_install' => 'Keep previous installation',
 		'next_step' => 'Go to the next step',
+		'reinstall' => 'Reinstall FreshRSS',
 	),
 	'auth' => array(
 		'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(
 		'_' => 'Checks',
+		'already_installed' => 'We have detected that FreshRSS is already installed!',
 		'cache' => array(
 			'nok' => 'Check permissions on <em>./data/cache</em> directory. HTTP server must have rights to write into',
 			'ok' => 'Permissions on cache directory are good.',
@@ -93,6 +96,9 @@ return array(
 	'delete_articles_after' => 'Remove articles after',
 	'fix_errors_before' => 'Please fix errors before skipping to the next step.',
 	'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',
 		'choose' => 'Choose a language for FreshRSS',

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

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

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

@@ -160,8 +160,15 @@ return array(
 		'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>',
 		'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_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',
 		'user_list' => 'Liste des utilisateurs',
 		'username' => 'Nom d’utilisateur',

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

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => '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>',
 		'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>',

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

@@ -102,6 +102,7 @@ return array(
 			'_' => 'L’utilisateur %s a été supprimé.',
 			'error' => 'L’utilisateur %s ne peut pas être supprimé.',
 		),
+		'set_registration' => 'Le nombre maximal de comptes a été mis à jour.',
 	),
 	'profile' => array(
 		'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',
 	),
 	'auth' => array(
+		'email' => 'Adresse courriel',
 		'keep_logged_in' => 'Rester connecté <small>(1 mois)</small>',
 		'login' => 'Connexion',
 		'login_persona' => 'Connexion avec Persona',
 		'login_persona_problem' => 'Problème de connexion à Persona ?',
 		'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',
-		'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.',
 	),
 	'date' => array(

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

@@ -4,7 +4,9 @@ return array(
 	'action' => array(
 		'finish' => 'Terminer l’installation',
 		'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',
+		'reinstall' => 'Réinstaller FreshRSS',
 	),
 	'auth' => array(
 		'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(
 		'_' => 'Vérifications',
+		'already_installed' => 'FreshRSS semble avoir déjà été installé !',
 		'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',
 			'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',
 	'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é',
+	'js' => array(
+		'confirm_reinstall' => 'Réinstaller FreshRSS vous fera perdre la configuration précédente. Êtes-vous sûr de vouloir continuer ?',
+	),
 	'language' => array(
 		'_' => 'Langue',
 		'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',
 		'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
 		'url' => 'URL du flux',
-		'validator' => 'Vérifier la valididé du flux',
+		'validator' => 'Vérifier la validité du flux',
 		'website' => 'URL du site',
+		'pubsubhubbub' => 'Notification instantanée par PubSubHubbub',
 	),
 	'import_export' => array(
 		'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_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'])) {
 	define('STEP',(int)$_GET['step']);
 } 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() {
+	$user_default_config = Minz_Configuration::get('default_user');
 	if (!empty($_POST)) {
 		$_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['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);
@@ -108,7 +152,7 @@ function saveStep2() {
 
 		$_SESSION['salt'] = sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
 		if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
-			$_SESSION['old_entries'] = 3;
+			$_SESSION['old_entries'] = $user_default_config->old_entries;
 		}
 
 		$token = '';
@@ -118,7 +162,7 @@ function saveStep2() {
 
 		$config_array = array(
 			'language' => $_SESSION['language'],
-			'theme' => 'Origine',
+			'theme' => $user_default_config->theme,
 			'old_entries' => $_SESSION['old_entries'],
 			'mail_login' => $_SESSION['mail_login'],
 			'passwordHash' => $_SESSION['passwordHash'],
@@ -165,14 +209,14 @@ function saveStep3() {
 			$_SESSION['bd_user'] = $_POST['user'];
 			$_SESSION['bd_password'] = $_POST['pass'];
 			$_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(
-			'environment' => 'production',
-			'simplepie_syslog_enabled' => true,
 			'salt' => $_SESSION['salt'],
+			'base_url' => $base_url,
 			'title' => $_SESSION['title'],
 			'default_user' => $_SESSION['default_user'],
 			'auth_type' => $_SESSION['auth_type'],
@@ -183,7 +227,9 @@ function saveStep3() {
 				'password' => $_SESSION['bd_password'],
 				'base' => $_SESSION['bd_base'],
 				'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
@@ -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() {
 	$conf = !empty($_SESSION['title']) &&
 	        !empty($_SESSION['old_entries']) &&
@@ -427,7 +500,7 @@ function printStep0() {
 		<div class="form-group">
 			<label class="group-name" for="language"><?php echo _t('install.language'); ?></label>
 			<div class="group-controls">
-				<select name="language" id="language">
+				<select name="language" id="language" tabindex="1" >
 				<?php foreach ($languages as $lang) { ?>
 				<option value="<?php echo $lang; ?>"<?php echo $actual == $lang ? ' selected="selected"' : ''; ?>>
 					<?php echo _t('gen.lang.' . $lang); ?>
@@ -439,10 +512,10 @@ function printStep0() {
 
 		<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>
+				<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') { ?>
-				<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 } ?>
 			</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>
 	<?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 { ?>
 	<p class="alert alert-error"><?php echo _t('install.action.fix_errors_before'); ?></p>
 	<?php } ?>
@@ -544,6 +647,7 @@ function printStep1() {
 }
 
 function printStep2() {
+	$user_default_config = Minz_Configuration::get('default_user');
 ?>
 	<?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>
@@ -557,28 +661,28 @@ function printStep2() {
 		<div class="form-group">
 			<label class="group-name" for="title"><?php echo _t('install.title'); ?></label>
 			<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 class="form-group">
 			<label class="group-name" for="old_entries"><?php echo _t('install.delete_articles_after'); ?></label>
 			<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 class="form-group">
 			<label class="group-name" for="default_user"><?php echo _t('install.default_user'); ?></label>
 			<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 class="form-group">
 			<label class="group-name" for="auth_type"><?php echo _t('install.auth.type'); ?></label>
 			<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
 						function no_auth($auth_type) {
 							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>
 			<div class="group-controls">
 				<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>
 				</div>
 				<?php echo _i('help'); ?> <?php echo _t('install.auth.password_format'); ?>
@@ -608,7 +712,7 @@ function printStep2() {
 		<div class="form-group">
 			<label class="group-name" for="mail_login"><?php echo _t('install.auth.email_persona'); ?></label>
 			<div class="group-controls">
-				<input type="email" id="mail_login" name="mail_login" value="<?php echo isset($_SESSION['mail_login']) ? $_SESSION['mail_login'] : ''; ?>" placeholder="alice@example.net" <?php echo $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>
 			</div>
 		</div>
@@ -636,7 +740,7 @@ function printStep2() {
 				toggles[i].addEventListener('mouseup', hide_password);
 			}
 
-			function auth_type_change(focus) {
+			function auth_type_change() {
 				var auth_value = document.getElementById('auth_type').value,
 				    password_input = document.getElementById('passwordPlain'),
 				    mail_input = document.getElementById('mail_login');
@@ -644,29 +748,23 @@ function printStep2() {
 				if (auth_value === 'form') {
 					password_input.required = true;
 					mail_input.required = false;
-					if (focus) {
-						password_input.focus();
-					}
 				} else if (auth_value === 'persona') {
 					password_input.required = false;
 					mail_input.required = true;
-					if (focus) {
-						mail_input.focus();
-					}
 				} else {
 					password_input.required = false;
 					mail_input.required = false;
 				}
 			}
-			auth_type_change(false);
+			auth_type_change();
 		</script>
 
 		<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>
+				<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') { ?>
-				<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 } ?>
 			</div>
 		</div>
@@ -675,6 +773,7 @@ function printStep2() {
 }
 
 function printStep3() {
+	$system_default_config = Minz_Configuration::get('default_system');
 ?>
 	<?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>
@@ -687,7 +786,7 @@ function printStep3() {
 		<div class="form-group">
 			<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
 			<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')) {?>
 				<option value="mysql"
 					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
@@ -708,35 +807,35 @@ function printStep3() {
 		<div class="form-group">
 			<label class="group-name" for="host"><?php echo _t('install.bdd.host'); ?></label>
 			<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 class="form-group">
 			<label class="group-name" for="user"><?php echo _t('install.bdd.username'); ?></label>
 			<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 class="form-group">
 			<label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label>
 			<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 class="form-group">
 			<label class="group-name" for="base"><?php echo _t('install.bdd'); ?></label>
 			<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 class="form-group">
 			<label class="group-name" for="prefix"><?php echo _t('install.bdd.prefix'); ?></label>
 			<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>
@@ -756,10 +855,10 @@ function printStep3() {
 
 		<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>
+				<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') { ?>
-				<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 } ?>
 			</div>
 		</div>
@@ -770,7 +869,7 @@ function printStep3() {
 function printStep4() {
 ?>
 	<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
 }
 
@@ -790,6 +889,7 @@ default:
 	saveLanguage();
 	break;
 case 1:
+	saveStep1();
 	break;
 case 2:
 	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 == 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 == 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>
 
 	<div class="post">

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

@@ -1,6 +1,10 @@
 <div class="prompt">
 	<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'); ?>">
 		<div>
 			<label for="username"><?php echo _t('gen.auth.username'); ?></label>

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

@@ -2,6 +2,10 @@
 <div class="prompt">
 	<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>
 		<a class="signin btn btn-important" href="<?php echo _url('auth', 'login'); ?>">
 			<?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>
 
 		<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" />
 		</div>
 		<div>

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

@@ -107,7 +107,7 @@
 		</div>
 		
 		<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">
 				<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>

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

@@ -9,7 +9,7 @@
 		<div class="form-group">
 			<label class="group-name" for="posts_per_page"><?php echo _t('conf.reading.articles_per_page'); ?></label>
 			<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'); ?>
 			</div>
 		</div>

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

@@ -67,7 +67,7 @@
 		<div class="form-group">
 			<label class="group-name" for="http_user"><?php echo _t('sub.feed.auth.username'); ?></label>
 			<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>
 
 			<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>
 			</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="group-controls">
 				<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>
 		<?php $auth = $this->feed->httpAuth(false); ?>
 		<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">
-				<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'); ?>
 			</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">
-				<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>
 

+ 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="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 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>
 				</ul>
 			</div>

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

@@ -3,6 +3,34 @@
 <div class="post">
 	<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'); ?>">
 		<legend><?php echo _t('admin.user.create'); ?></legend>
 

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

@@ -18,11 +18,11 @@
 		</div>
 
 		<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="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>
 				<?php echo _i('help'); ?> <?php echo _t('conf.profile.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
@@ -57,4 +57,35 @@
 			</div>
 		</div>
 	</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>

+ 2 - 1
constants.php

@@ -1,5 +1,5 @@
 <?php
-define('FRESHRSS_VERSION', '1.1.1-beta');
+define('FRESHRSS_VERSION', '1.1.2-dev');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 define('FRESHRSS_UPDATE_WEBSITE', 'https://update.freshrss.org?v=' . FRESHRSS_VERSION);
 define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
@@ -19,6 +19,7 @@ define('FRESHRSS_PATH', dirname(__FILE__));
 		define('UPDATE_FILENAME', DATA_PATH . '/update.php');
 		define('USERS_PATH', DATA_PATH . '/users');
 		define('CACHE_PATH', DATA_PATH . '/cache');
+		define('PSHB_PATH', DATA_PATH . '/PubSubHubbub');
 
 	define('LIB_PATH', FRESHRSS_PATH . '/lib');
 	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
 
-# 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(
 
 	# Set to `development` to get additional error messages,
@@ -11,9 +11,11 @@ return array(
 	# Used to make crypto more unique. Generated during install.
 	'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' => '',
 
 	# 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_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(
 
 		# Duration in seconds of the SimplePie cache,
@@ -75,6 +81,25 @@ return array(
 		# Max number of categories for a user.
 		'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(
@@ -83,7 +108,7 @@ return array(
 		'type' => 'sqlite',
 
 		# MySQL host.
-		'host' => '',
+		'host' => 'localhost',
 
 		# MySQL user.
 		'user' => '',
@@ -95,7 +120,13 @@ return array(
 		'base' => '',
 
 		# 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):
 	#	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',
 	'anon_access' => false,

+ 11 - 17
lib/Minz/Configuration.php

@@ -39,7 +39,7 @@ class Minz_Configuration {
 			throw new Minz_FileNotExistException($filename);
 		}
 
-		$data = @include($filename);
+		$data = include($filename);
 		if (is_array($data)) {
 			return $data;
 		} else {
@@ -84,11 +84,6 @@ class Minz_Configuration {
 	 */
 	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.
 	 */
@@ -119,21 +114,22 @@ class Minz_Configuration {
 	                             $configuration_setter = null) {
 		$this->namespace = $namespace;
 		$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 {
-			$this->data = self::load($this->config_filename);
+			$this->data = array_replace_recursive(
+				$this->data, self::load($this->config_filename)
+			);
 		} catch (Minz_FileNotExistException $e) {
-			if (is_null($default_filename)) {
+			if (is_null($this->default_filename)) {
 				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];
 		} elseif (!is_null($default)) {
 			return $default;
-		} elseif (isset($this->data_default[$key])) {
-			return $this->data_default[$key];
 		} else {
 			Minz_Log::warning($key . ' does not exist in configuration');
 			return null;

+ 1 - 1
lib/Minz/Extension.php

@@ -168,7 +168,7 @@ class Minz_Extension {
 		$url = '/ext.php?f=' . $file_name_url .
 		       '&amp;t=' . $type .
 		       '&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;
 		self::$sharedCurrentUser = $currentUser;
 
+		$driver_options = isset($conf->db['pdo_options']) && is_array($conf->db['pdo_options']) ? $conf->db['pdo_options'] : array();
+
 		try {
 			$type = $db['type'];
 			if ($type === 'mysql') {
 				$string = 'mysql:host=' . $db['host']
 				        . ';dbname=' . $db['base']
 				        . ';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 . '_';
 			} elseif ($type === '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 = '';
 			} else {
 				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 {
-			$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) {
 			$url_string = Minz_Request::getBaseUrl(PUBLIC_TO_INDEX_PATH);
+			if ($url_string === PUBLIC_TO_INDEX_PATH) {
+				$url_string = Minz_Request::guessBaseUrl();
+			}
 		} else {
 			$url_string = $isArray ? '.' : PUBLIC_RELATIVE;
 		}
 
 		if ($isArray) {
 			$url_string .= self::printUri($url, $encodage);
-		} else {
+		} elseif ($encodage === 'html') {
 			$url_string = Minz_Helper::htmlspecialchars_utf8($url_string . $url);
+		} else {
+			$url_string .= $url;
 		}
 
 		return $url_string;

+ 1 - 0
lib/Minz/View.php

@@ -91,6 +91,7 @@ class Minz_View {
 	 * Construit le layout
 	 */
 	public function buildLayout () {
+		header('Content-Type: text/html; charset=UTF-8');
 		$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()
 	 * @access private
 	 */
-	public $permanent_url = null;	//FreshRSS
+	public $permanent_url = null;
 
 	/**
 	 * @var object Instance of SimplePie_File to use as a feed
@@ -479,6 +479,13 @@ class SimplePie
 	 */
 	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
 	 * of cURL, even if a new enough version is installed
@@ -754,7 +761,7 @@ class SimplePie
 		else
 		{
 			$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)
 		{
 			$this->feed_url = $file->url;
-			$this->permanent_url = $this->feed_url;	//FreshRSS
+			$this->permanent_url = $this->feed_url;
 			$this->file =& $file;
 			return true;
 		}
@@ -807,6 +814,19 @@ class SimplePie
 	{
 		$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
@@ -1251,7 +1271,7 @@ class SimplePie
 		$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));
 	}
@@ -1297,7 +1317,7 @@ class SimplePie
 		// 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
 		$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))
 		{
@@ -1342,7 +1362,7 @@ class SimplePie
 			// Fetch the data via SimplePie_File into $this->raw_data
 			if (($fetched = $this->fetch_data($cache)) === true)
 			{
-				return $this->data['mtime'];	//FreshRSS
+				return $this->data['mtime'];
 			}
 			elseif ($fetched === false) {
 				return false;
@@ -1350,7 +1370,7 @@ class SimplePie
 
 			list($headers, $sniffed) = $fetched;
 
-			if (isset($this->data['md5']))	//FreshRSS
+			if (isset($this->data['md5']))
 			{
 				$md5 = $this->data['md5'];
 			}
@@ -1435,8 +1455,8 @@ class SimplePie
 						$this->data['headers'] = $headers;
 					}
 					$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
 					if ($cache && !$cache->save($this))
@@ -1451,7 +1471,7 @@ class SimplePie
 		if (isset($parser))
 		{
 			// 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
 		{
@@ -1477,7 +1497,7 @@ class SimplePie
 		{
 			// Load the Cache
 			$this->data = $cache->load();
-			if ($cache->mtime() + $this->cache_duration > time())	//FreshRSS
+			if ($cache->mtime() + $this->cache_duration > time())
 			{
 				$this->raw_data = false;
 				return true;	// If the cache is still valid, just return true
@@ -1514,71 +1534,58 @@ class SimplePie
 					}
 				}
 				// 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
 			{
-				//$cache->unlink();	//FreshRSS removed
-				$cache->touch();	//FreshRSS
+				$cache->touch();	//To keep the date/time of the last tentative update
 				$this->data = array();
 			}
 		}
@@ -1594,7 +1601,7 @@ class SimplePie
 				$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',
 				);
-				$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
@@ -1611,15 +1618,15 @@ class SimplePie
 
 			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
 				unset($file);
 				try
 				{
 					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__));
 						return false;
 					}
@@ -1634,8 +1641,8 @@ class SimplePie
 				if ($cache)
 				{
 					$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))
 					{
 						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->permanent_url = $file->permanent_url;	//FreshRSS
+		$this->permanent_url = $file->permanent_url;
 		$headers = $file->headers;
 		$sniffer = $this->registry->create('Content_Type_Sniffer', array(&$file));
 		$sniffed = $sniffer->get_type();
@@ -1852,7 +1859,7 @@ class SimplePie
 	 */
 	public function subscribe_url($permanent = false)
 	{
-		if ($permanent)	//FreshRSS
+		if ($permanent)
 		{
 			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()
 	{
-		//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()
 	{
-		//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 "\x0A":
 			case "\x0B":
-			case "\x0B":
 			case "\x0C":
 			case "\x20":
 			case "\x3C":

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

@@ -66,7 +66,7 @@ class SimplePie_File
 	var $method = SIMPLEPIE_FILE_SOURCE_NONE;
 	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'))
 		{
@@ -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']);
 		}
 		$this->url = $url;
-		$this->permanent_url = $url;	//FreshRSS
+		$this->permanent_url = $url;
 		$this->useragent = $useragent;
 		if (preg_match('/^http(s)?:\/\//i', $url))
 		{
@@ -113,12 +113,15 @@ class SimplePie_File
 				curl_setopt($fp, CURLOPT_REFERER, $url);
 				curl_setopt($fp, CURLOPT_USERAGENT, $useragent);
 				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', '>='))
 				{
 					curl_setopt($fp, CURLOPT_FOLLOWLOCATION, 1);
 					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);
 				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);
 							$previousStatusCode = $this->status_code;
 							$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;
 						}
 					}

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

@@ -406,6 +406,30 @@ class SimplePie_Item
 			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
@@ -738,31 +762,31 @@ class SimplePie_Item
 			{
 				$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'];
 			}
-			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'];
 			}
-			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'];
 			}
-			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'];
 			}
-			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'];
 			}
-			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'];
 			}
-			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'];
 			}
@@ -2733,7 +2757,9 @@ class SimplePie_Item
 						{
 							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))
 							{

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

@@ -148,7 +148,7 @@ class SimplePie_Locator
 		{
 			$sniffer = $this->registry->create('Content_Type_Sniffer', array($file));
 			$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;
 			}

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

@@ -79,8 +79,8 @@ class SimplePie_Misc
 
 	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;
 		}
 		$iri = SimplePie_IRI::absolutize(new SimplePie_IRI($base), $relative);
@@ -128,7 +128,7 @@ class SimplePie_Misc
 						{
 							$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)
 		{
 			$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'])
 		{

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

@@ -173,7 +173,7 @@ class SimplePie_Parse_Date
 		'aug' => 8,
 		'august' => 8,
 		'sep' => 9,
-		'september' => 8,
+		'september' => 9,
 		'oct' => 10,
 		'october' => 10,
 		'nov' => 11,
@@ -331,8 +331,8 @@ class SimplePie_Parse_Date
 		'CCT' => 23400,
 		'CDT' => -18000,
 		'CEDT' => 7200,
-		'CEST' => 7200,	//FreshRSS
 		'CET' => 3600,
+		'CEST' => 7200,
 		'CGST' => -7200,
 		'CGT' => -10800,
 		'CHADT' => 49500,
@@ -721,7 +721,7 @@ class SimplePie_Parse_Date
 		{
 			$output .= substr($string, $position, $pos - $position);
 			$position = $pos + 1;
-			if ($string[$pos - 1] !== '\\')
+			if ($pos === 0 || $string[$pos - 1] !== '\\')
 			{
 				$depth++;
 				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)
 	{
-		if (!is_subclass_of($class, $this->default[$type]))
+		if (!@is_subclass_of($class, $this->default[$type]))
 		{
 			return false;
 		}
@@ -222,4 +222,4 @@ class SimplePie_Registry
 		$result = call_user_func_array(array($class, $method), $parameters);
 		return $result;
 	}
-}
+}

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

@@ -249,7 +249,7 @@ class SimplePie_Sanitize
 		{
 			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))
 				{
 					$type |= SIMPLEPIE_CONSTRUCT_HTML;
@@ -280,7 +280,7 @@ class SimplePie_Sanitize
 				$document->loadHTML($data);
 				restore_error_handler();
 
-				$xpath = new DOMXPath($document);	//FreshRSS
+				$xpath = new DOMXPath($document);
 
 				// 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) {
 	// number_format does not seem to be Unicode-compatible
 	return str_replace(' ', ' ',  //Espace fine insécable
@@ -143,6 +170,7 @@ function customSimplePie() {
 	$simplePie->set_cache_location(CACHE_PATH);
 	$simplePie->set_cache_duration($limits['cache_duration']);
 	$simplePie->set_timeout($limits['timeout']);
+	$simplePie->set_curl_options($system_conf->curl_options);
 	$simplePie->strip_htmltags(array(
 		'base', 'blink', 'body', 'doctype', 'embed',
 		'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 */
 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));
-	$html = file_get_contents ($url);
+	$html = file_get_contents($url);
 
 	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);
 	} 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.
  *
@@ -446,3 +500,12 @@ function array_push_unique(&$array, $value) {
 function array_remove(&$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");