Browse Source

Merge branch 'dev' into beta

Marien Fressinaud 10 năm trước cách đây
mục cha
commit
5fbbfdea8a
76 tập tin đã thay đổi với 1233 bổ sung282 xóa
  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");