Browse Source

Merge pull request #1399 from FreshRSS/dev

Release 1.6.2
Alexandre Alapetite 9 years ago
parent
commit
5f637bd816
51 changed files with 460 additions and 148 deletions
  1. 22 1
      CHANGELOG.md
  2. 5 1
      CREDITS.md
  3. 11 2
      README.fr.md
  4. 10 1
      README.md
  5. 4 0
      app/Controllers/authController.php
  6. 4 0
      app/Controllers/feedController.php
  7. 4 2
      app/Controllers/importExportController.php
  8. 112 47
      app/Controllers/updateController.php
  9. 10 5
      app/Models/Auth.php
  10. 30 17
      app/Models/EntryDAO.php
  11. 1 0
      app/Models/Factory.php
  12. 4 1
      app/Models/UserDAO.php
  13. 4 0
      app/i18n/cz/admin.php
  14. 1 1
      app/i18n/cz/gen.php
  15. 4 0
      app/i18n/cz/install.php
  16. 4 0
      app/i18n/de/admin.php
  17. 1 1
      app/i18n/de/gen.php
  18. 4 0
      app/i18n/de/install.php
  19. 4 0
      app/i18n/en/admin.php
  20. 1 1
      app/i18n/en/gen.php
  21. 4 0
      app/i18n/en/install.php
  22. 4 0
      app/i18n/fr/admin.php
  23. 1 1
      app/i18n/fr/gen.php
  24. 4 0
      app/i18n/fr/install.php
  25. 4 0
      app/i18n/it/admin.php
  26. 1 1
      app/i18n/it/gen.php
  27. 4 0
      app/i18n/it/install.php
  28. 4 0
      app/i18n/nl/admin.php
  29. 1 1
      app/i18n/nl/gen.php
  30. 4 0
      app/i18n/nl/install.php
  31. 4 0
      app/i18n/ru/admin.php
  32. 1 1
      app/i18n/ru/gen.php
  33. 4 0
      app/i18n/ru/install.php
  34. 4 0
      app/i18n/tr/admin.php
  35. 1 1
      app/i18n/tr/gen.php
  36. 4 0
      app/i18n/tr/install.php
  37. 9 3
      app/install.php
  38. 1 1
      app/views/auth/formLogin.phtml
  39. 60 41
      app/views/helpers/export/articles.phtml
  40. 25 3
      cli/README.md
  41. 3 0
      cli/create-user.php
  42. 7 7
      cli/do-install.php
  43. 35 0
      cli/user-info.php
  44. 1 1
      constants.php
  45. 4 1
      data/config.default.php
  46. 3 1
      extensions/README.md
  47. 6 0
      lib/Minz/ModelPdo.php
  48. 5 2
      lib/favicons.php
  49. 8 2
      lib/lib_install.php
  50. 3 0
      lib/lib_rss.php
  51. 1 1
      p/scripts/main.js

+ 22 - 1
CHANGELOG.md

@@ -1,5 +1,26 @@
 # Changelog
 
+## 2016-12-26 FreshRSS 1.6.2
+
+* Features
+	* Add git compatibility in Web update system [#1357](https://github.com/FreshRSS/FreshRSS/issues/1357)
+		* Requires that the initial installation is done with git
+	* New option `limits.cookie_duration` in `data/config.php` to set the login cookie duration [#1384](https://github.com/FreshRSS/FreshRSS/issues/1384)
+* SQL
+	* More robust export function in the case of large datasets [#1372](https://github.com/FreshRSS/FreshRSS/issues/1372)
+* CLI
+	* New command `./cli/user-info.php` to get some user information [#1345](https://github.com/FreshRSS/FreshRSS/issues/1345)
+* Bug fixing
+	* Fix bug in estimating last user activity [#1358](https://github.com/FreshRSS/FreshRSS/issues/1358)
+	* PostgreSQL: fix bug when updating cached values [#1360](https://github.com/FreshRSS/FreshRSS/issues/1360)
+	* Fix bug in confirmation before marking as read [#1348](https://github.com/FreshRSS/FreshRSS/issues/1348)
+	* Fix small bugs in installer [#1363](https://github.com/FreshRSS/FreshRSS/pull/1363)
+	* Allow slash in database hostname, when using sockets [#1364](https://github.com/FreshRSS/FreshRSS/issues/1364)
+	* Add curl user-agent to retrieve favicons [#1380](https://github.com/FreshRSS/FreshRSS/issues/1380)
+	* Send login cookie only once [#1398](https://github.com/FreshRSS/FreshRSS/pull/1398)
+	* Add a check for PHP extension fileinfo [#1375](https://github.com/FreshRSS/FreshRSS/issues/1375)
+
+
 ## 2016-11-02 FreshRSS 1.6.1
 
 * Bug fixing
@@ -53,7 +74,7 @@
 	* Download icon 💾 for podcasts [#1236](https://github.com/FreshRSS/FreshRSS/issues/1236)
 * SimplePie
 	* Fix auto-discovery of RSS feeds in Web pages served as `text/xml` [#1264](https://github.com/FreshRSS/FreshRSS/issues/1264)
-* Mics.
+* Misc.
 	* Removed *resource-priorities* attributes (`defer`, `lazyload`), deprecated by W3C [#1222](https://github.com/FreshRSS/FreshRSS/pull/1222)
 
 

+ 5 - 1
CREDITS.md

@@ -1,5 +1,5 @@
 This is a credit file of people who have [contributed to FreshRSS](https://github.com/FreshRSS/FreshRSS/graphs/contributors) with, at least,
-one commit on the FreshRSS repository (at https://github.com/FreshRSS/FreshRSS).
+one commit on one of the FreshRSS repositories (at https://github.com/FreshRSS).
 Please note a commit on THIS specific file is not considered as a contribution
 (too easy!). Its purpose is to show that even the smallest contribution is important.
 People are sorted by name so please keep this order.
@@ -19,15 +19,19 @@ People are sorted by name so please keep this order.
 * [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb)
 * [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=edhelas), [Web](http://edhelas.movim.eu/)
 * [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/)
+* [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=kevinpapst), [Web](http://www.kevinpapst.de/)
 * [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ldidry), [Web](https://www.fiat-tux.fr/)
 * [marcomrc](https://github.com/marcomrc): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=marcomrc)
 * [Marcus Rohrmoser](https://github.com/mro): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=mro), [Web](http://mro.name/~me)
 * [Marien Fressinaud](https://github.com/marienfressinaud): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=marienfressinaud), [Web](http://marienfressinaud.fr/)
 * [Melvyn Laïly](https://github.com/yaurthek): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=yaurthek), [Web](http://x2a.yt/)
 * [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicolaselie)
+* [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
 * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)
+* [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/)
 * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi)
+* [subic](https://github.com/subic): [contributions](https://github.com/FreshRSS/documentation/commits?author=subic)
 * [Tets42](https://github.com/Tets42): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Tets42)
 * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue)
 * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo)

+ 11 - 2
README.fr.md

@@ -8,6 +8,7 @@ Il se veut léger et facile à prendre en main tout en étant un outil puissant
 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.
 Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de commande](./cli/README.md).
+Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
 
 * Site officiel : http://freshrss.org
 * Démo : http://demo.freshrss.org/
@@ -23,7 +24,7 @@ Voir la [liste des versions](../../releases).
 * Pour ceux qui veulent bien aider à tester ou déveloper les dernières fonctionnalités, [la branche dev](https://github.com/FreshRSS/FreshRSS/tree/dev) vous ouvre les bras !
 
 # Avertissements
-Cette application a été développée pour s’adapter principalement à des besoins personnels, et aucune garantie n'est fournie.
+Cette application a été développée pour s’adapter principalement à des besoins personnels, et aucune garantie nest fournie.
 Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
 Nous sommes une communauté amicale.
 
@@ -85,13 +86,16 @@ sudo git checkout -b dev origin/dev
 
 # Mettre les droits d’accès pour le serveur Web
 sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
+# Si vous souhaitez permettre les mises à jour par l’interface Web
+sudo chmod -R g+w .
+
 # Publier FreshRSS dans votre répertoire HTML public
 sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
 # Naviguez vers http://example.net/FreshRSS pour terminer l’installation
 # (Si vous le faite depuis localhost, vous pourrez avoir à ajuster le réglage de votre adresse publique)
 # ou utilisez l’interface en ligne de commande
 
-# Mettre à jour FreshRSS vers une nouvelle version
+# Mettre à jour FreshRSS vers une nouvelle version par git
 cd /usr/share/FreshRSS
 sudo git pull
 sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
@@ -140,6 +144,11 @@ mysqldump -u utilisateur -p --databases freshrss > freshrss.sql
 ```
 
 
+# Extensions 
+FreshRSS permet l’ajout d’extensions en plus des fonctionnalités natives.
+Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensions).
+
+
 # Bibliothèques incluses
 * [SimplePie](http://simplepie.org/)
 * [MINZ](https://github.com/marienfressinaud/MINZ)

+ 10 - 1
README.md

@@ -8,6 +8,7 @@ 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.
 There is an API for (mobile) clients, and a [Command-Line Interface](./cli/README.md).
+Finally, it supports [extensions](#extensions) for further tuning.
 
 * Official website: http://freshrss.org
 * Demo: http://demo.freshrss.org/
@@ -85,13 +86,16 @@ sudo git checkout -b dev origin/dev
 
 # Set the rights so that your Web server can access the files
 sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
+# If you would like to allow updates from the Web interface
+sudo chmod -R g+w .
+
 # Publish FreshRSS in your public HTML directory
 sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
 # Navigate to http://example.net/FreshRSS to complete the installation
 # (If you do it from localhost, you may have to adjust the setting of your public address later)
 # or use the Command-Line Interface
 
-# Update to a newer version of FreshRSS
+# Update to a newer version of FreshRSS with git
 cd /usr/share/FreshRSS
 sudo git pull
 sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
@@ -140,6 +144,11 @@ mysqldump -u user -p --databases freshrss > freshrss.sql
 ```
 
 
+# Extensions 
+FreshRSS supports further customizations by adding extensions on top of its core functionality.
+See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions). 
+
+
 # Included libraries
 * [SimplePie](http://simplepie.org/)
 * [MINZ](https://github.com/marienfressinaud/MINZ)

+ 4 - 0
app/Controllers/authController.php

@@ -113,6 +113,10 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 		$file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js');
 		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime));
 
+		$conf = Minz_Configuration::get('system');
+		$limits = $conf->limits;
+		$this->view->cookie_days = round($limits['cookie_duration'] / 86400, 1);
+
 		if (Minz_Request::isPost()) {
 			$nonce = Minz_Session::param('nonce');
 			$username = Minz_Request::param('username', '');

+ 4 - 0
app/Controllers/feedController.php

@@ -27,6 +27,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 	}
 
 	public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') {
+		FreshRSS_UserDAO::touch();
 		@set_time_limit(300);
 
 		$catDAO = new FreshRSS_CategoryDAO();
@@ -484,6 +485,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		if ($feed_id <= 0 || $feed_name == '') {
 			return false;
 		}
+		FreshRSS_UserDAO::touch();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		return $feedDAO->updateFeed($feed_id, array('name' => $feed_name));
 	}
@@ -492,6 +494,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		if ($feed_id <= 0 || ($cat_id <= 0 && $new_cat_name == '')) {
 			return false;
 		}
+		FreshRSS_UserDAO::touch();
 
 		$catDAO = new FreshRSS_CategoryDAO();
 		if ($cat_id > 0) {
@@ -540,6 +543,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 	}
 
 	public static function deleteFeed($feed_id) {
+		FreshRSS_UserDAO::touch();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		if ($feedDAO->deleteFeed($feed_id)) {
 			// TODO: Delete old favicon

+ 4 - 2
app/Controllers/importExportController.php

@@ -531,6 +531,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 
+		$this->entryDAO->disableBuffering();
+
 		if ($export_feeds === true) {
 			//All feeds
 			$export_feeds = $this->feedDAO->listFeedsIds();
@@ -641,13 +643,13 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			$this->view->list_title = _t('sub.import_export.starred_list');
 			$this->view->type = 'starred';
 			$unread_fav = $this->entryDAO->countUnreadReadFavorites();
-			$this->view->entries = $this->entryDAO->listWhere(
+			$this->view->entriesRaw = $this->entryDAO->listWhereRaw(
 				's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all']
 			);
 		} elseif ($type === 'feed' && $feed != null) {
 			$this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
 			$this->view->type = 'feed/' . $feed->id();
-			$this->view->entries = $this->entryDAO->listWhere(
+			$this->view->entriesRaw = $this->entryDAO->listWhereRaw(
 				'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
 				$maxFeedEntries
 			);

+ 112 - 47
app/Controllers/updateController.php

@@ -2,6 +2,45 @@
 
 class FreshRSS_update_Controller extends Minz_ActionController {
 
+	public static function isGit() {
+		return is_dir(FRESHRSS_PATH . '/.git/');
+	}
+
+	public static function hasGitUpdate() {
+		$cwd = getcwd();
+		chdir(FRESHRSS_PATH);
+		$output = array();
+		try {
+			exec('git fetch', $output, $return);
+			if ($return == 0) {
+				exec('git status -sb --porcelain remote', $output, $return);
+			} else {
+				$line = is_array($output) ? implode('; ', $output) : '' . $output;
+				Minz_Log::warning('git fetch warning:' . $line);
+			}
+		} catch (Exception $e) {
+			Minz_Log::warning('git fetch error:' . $e->getMessage());
+		}
+		chdir($cwd);
+		$line = is_array($output) ? implode('; ', $output) : '' . $output;
+		return strpos($line, '[behind') !== false;
+	}
+
+	public static function gitPull() {
+		$cwd = getcwd();
+		chdir(FRESHRSS_PATH);
+		$output = array();
+		$return = 1;
+		try {
+			exec('git pull --ff-only', $output, $return);
+		} catch (Exception $e) {
+			Minz_Log::warning('git pull error:' . $e->getMessage());
+		}
+		chdir($cwd);
+		$line = is_array($output) ? implode('; ', $output) : '' . $output;
+		return $return == 0 ? true : 'Git error: ' . $line;
+	}
+
 	public function firstAction() {
 		if (!FreshRSS_Auth::hasAccess('admin')) {
 			Minz_Error::error(403);
@@ -20,7 +59,7 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 	public function indexAction() {
 		Minz_View::prependTitle(_t('admin.update.title') . ' · ');
 
-		if (file_exists(UPDATE_FILENAME) && !is_writable(FRESHRSS_PATH)) {
+		if (!is_writable(FRESHRSS_PATH)) {
 			$this->view->message = array(
 				'status' => 'bad',
 				'title' => _t('gen.short.damn'),
@@ -53,49 +92,65 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 			return;
 		}
 
-		$auto_update_url = FreshRSS_Context::$system_conf->auto_update_url . '?v=' . FRESHRSS_VERSION;
-		$c = curl_init($auto_update_url);
-		curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
-		curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true);
-		curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 2);
-		$result = curl_exec($c);
-		$c_status = curl_getinfo($c, CURLINFO_HTTP_CODE);
-		$c_error = curl_error($c);
-		curl_close($c);
-
-		if ($c_status !== 200) {
-			Minz_Log::warning(
-				'Error during update (HTTP code ' . $c_status . '): ' . $c_error
-			);
+		$script = '';
+		$version = '';
 
-			$this->view->message = array(
-				'status' => 'bad',
-				'title' => _t('gen.short.damn'),
-				'body' => _t('feedback.update.server_not_found', $auto_update_url)
-			);
-			return;
-		}
-
-		$res_array = explode("\n", $result, 2);
-		$status = $res_array[0];
-		if (strpos($status, 'UPDATE') !== 0) {
-			$this->view->message = array(
-				'status' => 'bad',
-				'title' => _t('gen.short.damn'),
-				'body' => _t('feedback.update.none')
-			);
+		if (self::isGit()) {
+			if (self::hasGitUpdate()) {
+				$version = 'git';
+			} else {
+				$this->view->message = array(
+					'status' => 'bad',
+					'title' => _t('gen.short.damn'),
+					'body' => _t('feedback.update.none')
+				);
+				@touch(join_path(DATA_PATH, 'last_update.txt'));
+				return;
+			}
+		} else {
+			$auto_update_url = FreshRSS_Context::$system_conf->auto_update_url . '?v=' . FRESHRSS_VERSION;
+			Minz_Log::debug('HTTP GET ' . $auto_update_url);
+			$c = curl_init($auto_update_url);
+			curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
+			curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true);
+			curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 2);
+			$result = curl_exec($c);
+			$c_status = curl_getinfo($c, CURLINFO_HTTP_CODE);
+			$c_error = curl_error($c);
+			curl_close($c);
+
+			if ($c_status !== 200) {
+				Minz_Log::warning(
+					'Error during update (HTTP code ' . $c_status . '): ' . $c_error
+				);
+
+				$this->view->message = array(
+					'status' => 'bad',
+					'title' => _t('gen.short.damn'),
+					'body' => _t('feedback.update.server_not_found', $auto_update_url)
+				);
+				return;
+			}
 
-			@touch(join_path(DATA_PATH, 'last_update.txt'));
+			$res_array = explode("\n", $result, 2);
+			$status = $res_array[0];
+			if (strpos($status, 'UPDATE') !== 0) {
+				$this->view->message = array(
+					'status' => 'bad',
+					'title' => _t('gen.short.damn'),
+					'body' => _t('feedback.update.none')
+				);
+				@touch(join_path(DATA_PATH, 'last_update.txt'));
+				return;
+			}
 
-			return;
+			$script = $res_array[1];
+			$version = explode(' ', $status, 2);
+			$version = $version[1];
 		}
 
-		$script = $res_array[1];
 		if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
-			$version = explode(' ', $status, 2);
-			$version = $version[1];
 			@file_put_contents(join_path(DATA_PATH, 'last_update.txt'), $version);
-
 			Minz_Request::forward(array('c' => 'update'), true);
 		} else {
 			$this->view->message = array(
@@ -111,10 +166,13 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 			Minz_Request::forward(array('c' => 'update'), true);
 		}
 
-		require(UPDATE_FILENAME);
-
 		if (Minz_Request::param('post_conf', false)) {
-			$res = do_post_update();
+			if (self::isGit()) {
+				$res = !self::hasGitUpdate();
+			} else {
+				require(UPDATE_FILENAME);
+				$res = do_post_update();
+			}
 
 			Minz_ExtensionManager::callHook('post_update');
 
@@ -126,14 +184,21 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 				Minz_Request::bad(_t('feedback.update.error', $res),
 				                  array('c' => 'update', 'a' => 'index'));
 			}
-		}
-
-		if (Minz_Request::isPost()) {
-			save_info_update();
-		}
+		} else {
+			$res = false;
 
-		if (!need_info_update()) {
-			$res = apply_update();
+			if (self::isGit()) {
+				$res = self::gitPull();
+			} else {
+				if (Minz_Request::isPost()) {
+					save_info_update();
+				}
+				if (!need_info_update()) {
+					$res = apply_update();
+				} else {
+					return;
+				}
+			}
 
 			if ($res === true) {
 				Minz_Request::forward(array(

+ 10 - 5
app/Models/Auth.php

@@ -25,7 +25,7 @@ class FreshRSS_Auth {
 			self::giveAccess();
 		} elseif (self::accessControl()) {
 			self::giveAccess();
-			FreshRSS_UserDAO::touch($current_user);
+			FreshRSS_UserDAO::touch();
 		} else {
 			// Be sure all accesses are removed!
 			self::removeAccess();
@@ -219,8 +219,8 @@ class FreshRSS_FormAuth {
 	}
 
 	public static function makeCookie($username, $password_hash) {
+		$conf = Minz_Configuration::get('system');
 		do {
-			$conf = Minz_Configuration::get('system');
 			$token = sha1($conf->salt . $username . uniqid(mt_rand(), true));
 			$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
 		} while (file_exists($token_file));
@@ -229,15 +229,17 @@ class FreshRSS_FormAuth {
 			return false;
 		}
 
-		$expire = time() + 2629744;	//1 month	//TODO: Use a configuration instead
+		$limits = $conf->limits;
+		$cookie_duration = empty($limits['cookie_duration']) ? 2629744 : $limits['cookie_duration'];
+		$expire = time() + $cookie_duration;
 		Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
 		return $token;
 	}
 
 	public static function deleteCookie() {
 		$token = Minz_Session::getLongTermCookie('FreshRSS_login');
-		Minz_Session::deleteLongTermCookie('FreshRSS_login');
 		if (ctype_alnum($token)) {
+			Minz_Session::deleteLongTermCookie('FreshRSS_login');
 			@unlink(DATA_PATH . '/tokens/' . $token . '.txt');
 		}
 
@@ -247,7 +249,10 @@ class FreshRSS_FormAuth {
 	}
 
 	public static function purgeTokens() {
-		$oldest = time() - 2629744;	// 1 month	// TODO: Use a configuration instead
+		$conf = Minz_Configuration::get('system');
+		$limits = $conf->limits;
+		$cookie_duration = empty($limits['cookie_duration']) ? 2629744 : $limits['cookie_duration'];
+		$oldest = time() - $cookie_duration;
 		foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {
 			// $extension = $file_info->getExtension(); doesn't work in PHP < 5.3.7
 			$extension = pathinfo($file_info->getFilename(), PATHINFO_EXTENSION);

+ 30 - 17
app/Models/EntryDAO.php

@@ -241,6 +241,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if (count($ids) < 1) {
 			return 0;
 		}
+		FreshRSS_UserDAO::touch();
 		$sql = 'UPDATE `' . $this->prefix . 'entry` '
 		     . 'SET is_favorite=? '
 		     . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)';
@@ -315,6 +316,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @return integer affected rows
 	 */
 	public function markRead($ids, $is_read = true) {
+		FreshRSS_UserDAO::touch();
 		if (is_array($ids)) {	//Many IDs at once (used by API)
 			if (count($ids) < 6) {	//Speed heuristics
 				$affected = 0;
@@ -379,6 +381,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @return integer affected rows
 	 */
 	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
+		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
@@ -421,6 +424,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @return integer affected rows
 	 */
 	public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
+		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadCat(0) is deprecated!');
@@ -458,6 +462,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @return integer affected rows
 	 */
 	public function markReadFeed($id_feed, $idMax = 0, $filter = null, $state = 0) {
+		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadFeed(0) is deprecated!');
@@ -513,7 +518,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		$entries = self::daoToEntry($res);
+		$entries = self::daoToEntries($res);
 		return isset($entries[0]) ? $entries[0] : null;
 	}
 
@@ -528,7 +533,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 		$stm->execute($values);
 		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		$entries = self::daoToEntry($res);
+		$entries = self::daoToEntries($res);
 		return isset($entries[0]) ? $entries[0] : null;
 	}
 
@@ -661,7 +666,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			. ($limit > 0 ? ' LIMIT ' . $limit : ''));	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
 	}
 
-	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
+	public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
 		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
 
 		$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
@@ -675,8 +680,12 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 		$stm = $this->bd->prepare($sql);
 		$stm->execute($values);
+		return $stm;
+	}
 
-		return self::daoToEntry($stm->fetchAll(PDO::FETCH_ASSOC));
+	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
+		$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
 	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {	//For API
@@ -805,15 +814,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $res[0];
 	}
 
-	public static function daoToEntry($listDAO) {
-		$list = array();
-
-		if (!is_array($listDAO)) {
-			$listDAO = array($listDAO);
-		}
-
-		foreach ($listDAO as $key => $dao) {
-			$entry = new FreshRSS_Entry(
+	public static function daoToEntry($dao) {
+		$entry = new FreshRSS_Entry(
 				$dao['id_feed'],
 				$dao['guid'],
 				$dao['title'],
@@ -825,10 +827,21 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				$dao['is_favorite'],
 				$dao['tags']
 			);
-			if (isset($dao['id'])) {
-				$entry->_id($dao['id']);
-			}
-			$list[] = $entry;
+		if (isset($dao['id'])) {
+			$entry->_id($dao['id']);
+		}
+		return $entry;
+	}
+
+	private static function daoToEntries($listDAO) {
+		$list = array();
+
+		if (!is_array($listDAO)) {
+			$listDAO = array($listDAO);
+		}
+
+		foreach ($listDAO as $key => $dao) {
+			$list[] = self::daoToEntry($dao);
 		}
 
 		unset($listDAO);

+ 1 - 0
app/Models/Factory.php

@@ -6,6 +6,7 @@ class FreshRSS_Factory {
 		$conf = Minz_Configuration::get('system');
 		switch ($conf->db['type']) {
 			case 'sqlite':
+			case 'pgsql':
 				return new FreshRSS_FeedDAOSQLite($username);
 			default:
 				return new FreshRSS_FeedDAO($username);

+ 4 - 1
app/Models/UserDAO.php

@@ -84,7 +84,10 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		return is_dir(join_path(DATA_PATH , 'users', $username));
 	}
 
-	public static function touch($username) {
+	public static function touch($username = '') {
+		if (($username == '') || (!ctype_alnum($username))) {
+			$username = Minz_Session::param('currentUser', '_');
+		}
 		return touch(join_path(DATA_PATH , 'users', $username, 'config.php'));
 	}
 

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

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'Tabulka kanálů je nastavena špatně.',
 			'ok' => 'Tabulka kanálů je v pořádku.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Nemáte PHP fileinfo (balíček fileinfo).',
+			'ok' => 'Máte rozšíření fileinfo.',
+		),
 		'files' => 'Instalace souborů',
 		'json' => array(
 			'nok' => 'Nemáte JSON (balíček php5-json).',

+ 1 - 1
app/i18n/cz/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'Email',
-		'keep_logged_in' => 'Zapamatovat přihlášení <small>(1 měsíc)</small>',
+		'keep_logged_in' => 'Zapamatovat přihlášení <small>(%s dny)</small>',
 		'login' => 'Login',
 		'logout' => 'Odhlášení',
 		'password' =>  array(

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

@@ -56,6 +56,10 @@ return array(
 			'nok' => 'Zkontrolujte oprávnění adresáře <em>./data/favicons</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
 			'ok' => 'Oprávnění adresáře favicons jsou v pořádku.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Nemáte PHP fileinfo (balíček fileinfo).',
+			'ok' => 'Máte rozšíření fileinfo.',
+		),
 		'http_referer' => array(
 			'nok' => 'Zkontrolujte prosím že neměníte HTTP REFERER.',
 			'ok' => 'Váš HTTP REFERER je znám a odpovídá Vašemu serveru.',

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

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'Die Tabelle <em>feed</em> ist schlecht konfiguriert.',
 			'ok' => 'Die Tabelle <em>feed</em> ist korrekt konfiguriert.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Ihnen fehlt PHP fileinfo (Paket fileinfo).',
+			'ok' => 'Sie haben die fileinfo-Erweiterung.',
+		),
 		'files' => 'Datei-Installation',
 		'json' => array(
 			'nok' => 'Ihnen fehlt die JSON-Erweiterung (Paket php5-json).',

+ 1 - 1
app/i18n/de/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'E-Mail-Adresse',
-		'keep_logged_in' => 'Eingeloggt bleiben <small>(1 Monat)</small>',
+		'keep_logged_in' => 'Eingeloggt bleiben <small>(%s Tage)</small>',
 		'login' => 'Anmelden',
 		'logout' => 'Abmelden',
 		'password' => array(

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

@@ -56,6 +56,10 @@ return array(
 			'nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>./data/favicons</em>. Der HTTP-Server muss Schreibrechte besitzen.',
 			'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/favicons</em> sind in Ordnung.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Ihnen fehlt PHP fileinfo (Paket fileinfo).',
+			'ok' => 'Sie haben die fileinfo-Erweiterung.',
+		),
 		'http_referer' => array(
 			'nok' => 'Bitte stellen Sie sicher, dass Sie Ihren HTTP REFERER nicht abändern.',
 			'ok' => 'Ihr HTTP REFERER ist bekannt und entspricht Ihrem Server.',

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

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'Feed table is bad configured.',
 			'ok' => 'Feed table is ok.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Cannot find the PHP fileinfo library (fileinfo package).',
+			'ok' => 'You have the fileinfo library.',
+		),
 		'files' => 'File installation',
 		'json' => array(
 			'nok' => 'Cannot find JSON (php5-json package).',

+ 1 - 1
app/i18n/en/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'Email address',
-		'keep_logged_in' => 'Keep me logged in <small>(1 month)</small>',
+		'keep_logged_in' => 'Keep me logged in <small>(%s days)</small>',
 		'login' => 'Login',
 		'logout' => 'Logout',
 		'password' => array(

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

@@ -56,6 +56,10 @@ return array(
 			'nok' => 'Check permissions on <em>./data/favicons</em> directory. HTTP server must have rights to write into',
 			'ok' => 'Permissions on favicons directory are good.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Cannot find the PHP fileinfo library (fileinfo package).',
+			'ok' => 'You have the fileinfo library.',
+		),
 		'http_referer' => array(
 			'nok' => 'Please check that you are not altering your HTTP REFERER.',
 			'ok' => 'Your HTTP REFERER is known and corresponds to your server.',

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

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'La table feed est mal configurée.',
 			'ok' => 'La table feed est bien configurée.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Impossible de trouver la librairie PHP fileinfo (paquet fileinfo).',
+			'ok' => 'Vous disposez de la librairie fileinfo.',
+		),
 		'files' => 'Installation des fichiers',
 		'json' => array(
 			'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).',

+ 1 - 1
app/i18n/fr/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'Adresse courriel',
-		'keep_logged_in' => 'Rester connecté <small>(1 mois)</small>',
+		'keep_logged_in' => 'Rester connecté <small>(%s jours)</small>',
 		'login' => 'Connexion',
 		'logout' => 'Déconnexion',
 		'password' => array(

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

@@ -56,6 +56,10 @@ return array(
 			'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/favicons</em>. Le serveur HTTP doit être capable d’écrire dedans',
 			'ok' => 'Les droits sur le répertoire des favicons sont bons.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Vous ne disposez pas de PHP fileinfo (paquet fileinfo).',
+			'ok' => 'Vous disposez de fileinfo.',
+		),
 		'http_referer' => array(
 			'nok' => 'Veuillez vérifier que vous ne modifiez pas votre HTTP REFERER.',
 			'ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.',

+ 4 - 0
app/i18n/it/admin.php

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'La tabella Feed ha una configurazione errata.',
 			'ok' => 'Tabella Feed OK.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Manca il supporto per PHP fileinfo (pacchetto fileinfo).',
+			'ok' => 'Estensione fileinfo presente.',
+		),
 		'files' => 'Installazione files',
 		'json' => array(
 			'nok' => 'Manca il supoorto a JSON (pacchetto php5-json).',

+ 1 - 1
app/i18n/it/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'Indirizzo email',
-		'keep_logged_in' => 'Ricorda i dati <small>(1 mese)</small>',
+		'keep_logged_in' => 'Ricorda i dati <small>(%s giorni)</small>',
 		'login' => 'Accedi',
 		'logout' => 'Esci',
 		'password' => array(

+ 4 - 0
app/i18n/it/install.php

@@ -56,6 +56,10 @@ return array(
 			'nok' => 'Verifica i permessi sulla cartella <em>./data/favicons</em>. Il server HTTP deve avere i permessi per scriverci dentro',
 			'ok' => 'I permessi sulla cartella favicons sono corretti.',
 		),
+		'fileinfo' => array(
+			'nok' => 'Manca il supporto per PHP fileinfo (pacchetto fileinfo).',
+			'ok' => 'Estensione fileinfo presente.',
+		),
 		'http_referer' => array(
 			'nok' => 'Per favore verifica che non stai alterando il tuo HTTP REFERER.',
 			'ok' => 'Il tuo HTTP REFERER riconosciuto corrisponde al tuo server.',

+ 4 - 0
app/i18n/nl/admin.php

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'Feed tabel is slecht geconfigureerd.',
 			'ok' => 'Feed tabel is ok.',
 		),
+		'fileinfo' => array(
+			'nok' => 'U mist de PHP fileinfo (fileinfo package).',
+			'ok' => 'U hebt de fileinfo uitbreiding.',
+		),
 		'files' => 'Bestanden installatie',
 		'json' => array(
 			'nok' => 'U mist JSON (php5-json package).',

+ 1 - 1
app/i18n/nl/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'Email adres',
-		'keep_logged_in' => 'Ingelogd blijven voor <small>(1 maand)</small>',
+		'keep_logged_in' => 'Ingelogd blijven voor <small>(%s dagen)</small>',
 		'login' => 'Log in',
 		'logout' => 'Log uit',
 		'password' => array(

+ 4 - 0
app/i18n/nl/install.php

@@ -56,6 +56,10 @@ return array(
 			'nok' => 'Controleer permissies van de <em>./data/favicons</em> map. HTTP server moet rechten hebben om er in te kunnen schrijven',
 			'ok' => 'Permissies van de favicons map zijn goed.',
 		),
+		'fileinfo' => array(
+			'nok' => 'U mist PHP fileinfo (fileinfo package).',
+			'ok' => 'U hebt de fileinfo uitbreiding.',
+		),
 		'http_referer' => array(
 			'nok' => 'Controleer a.u.b. dat u niet uw HTTP REFERER wijzigd.',
 			'ok' => 'Uw HTTP REFERER is bekend en komt overeen met uw server.',

+ 4 - 0
app/i18n/ru/admin.php

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'Таблица подписок (feed) неправильно настроена.',
 			'ok' => 'Таблица подписок (feed) настроена правильно.',
 		),
+		'fileinfo' => array(
+			'nok' => 'У вас не установлено расширение PHP fileinfo (пакет fileinfo).',
+			'ok' => 'У вас установлено расширение fileinfo.',
+		),
 		'files' => 'Установка файлов',
 		'json' => array(
 			'nok' => 'У вас не установлена библиотека для работы с JSON (пакет php5-json).',

+ 1 - 1
app/i18n/ru/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'Email address',
-		'keep_logged_in' => 'Keep me logged in <small>(1 month)</small>',
+		'keep_logged_in' => 'Keep me logged in <small>(%s дней)</small>',
 		'login' => 'Login',
 		'logout' => 'Logout',
 		'password' => array(

+ 4 - 0
app/i18n/ru/install.php

@@ -56,6 +56,10 @@ return array(
 			'nok' => 'Проверьте права доступа к папке <em>./data/favicons</em> . Сервер HTTP должен иметь права на запись в эту папку.',
 			'ok' => 'Права на папку значков в порядке.',
 		),
+		'fileinfo' => array(
+			'nok' => 'У вас нет расширения PHP fileinfo (пакет fileinfo).',
+			'ok' => 'У вас установлено расширение fileinfo.',
+		),
 		'http_referer' => array(
 			'nok' => 'Убедитесь, что вы не изменяете ваш HTTP REFERER.',
 			'ok' => 'Ваш HTTP REFERER известен и соотвествует вашему серверу.',

+ 4 - 0
app/i18n/tr/admin.php

@@ -57,6 +57,10 @@ return array(
 			'nok' => 'Akış tablosu kötü yapılandırılmış.',
 			'ok' => 'Akış tablosu sorunsuz.',
 		),
+		'fileinfo' => array(
+			'nok' => 'PHP fileinfo eksik (fileinfo package).',
+			'ok' => 'fileinfo eklentisi sorunsuz.',
+		),
 		'files' => 'Dosya kurulumu',
 		'json' => array(
 			'nok' => 'JSON eklentisi eksik (php5-json package).',

+ 1 - 1
app/i18n/tr/gen.php

@@ -22,7 +22,7 @@ return array(
 	),
 	'auth' => array(
 		'email' => 'Email adresleri',
-		'keep_logged_in' => '<small>(1 ay)</small> oturumu açık tut',
+		'keep_logged_in' => '<small>(%s günler)</small> oturumu açık tut',
 		'login' => 'Giriş',
 		'logout' => 'Çıkış',
 		'password' => array(

+ 4 - 0
app/i18n/tr/install.php

@@ -56,6 +56,10 @@ return array(
 			'nok' => '<em>./data/favicons</em> klasör yetkisini kontrol edin. HTTP yazma yetkisi olmalı',
 			'ok' => 'Site ikonu klasörü yetkileri sorunsuz.',
 		),
+		'fileinfo' => array(
+			'nok' => 'PHP fileinfo eksik (fileinfo package).',
+			'ok' => 'fileinfo eklentisi sorunsuz.',
+		),
 		'http_referer' => array(
 			'nok' => 'Lütfen HTTP REFERER değiştirmediğinize emin olun.',
 			'ok' => 'HTTP REFERER ve sunucunuz arası iletişim sorunsuz.',

+ 9 - 3
app/install.php

@@ -230,7 +230,7 @@ function saveStep3() {
 			$_SESSION['bd_error'] = '';
 			header('Location: index.php?step=4');
 		} else {
-			$_SESSION['bd_error'] = empty($config_array['db']['bd_error']) ? 'Unknown error!' : $config_array['db']['bd_error'];
+			$_SESSION['bd_error'] = empty($config_array['db']['error']) ? 'Unknown error!' : $config_array['db']['error'];
 		}
 	}
 	invalidateHttpCache();
@@ -375,7 +375,7 @@ function checkDbUser(&$dbOptions) {
 		}
 	} catch (PDOException $e) {
 		$ok = false;
-		$dbOptions['bd_error'] = $e->getMessage();
+		$dbOptions['error'] = $e->getMessage();
 	}
 	return $ok;
 }
@@ -478,6 +478,12 @@ function printStep1() {
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.check.xml.nok'); ?></p>
 	<?php } ?>
 
+	<?php if ($res['fileinfo'] == 'ok') { ?>
+	<p class="alert alert-success"><span class="alert-head"><?php echo _t('gen.short.ok'); ?></span> <?php echo _t('install.check.fileinfo.ok'); ?></p>
+	<?php } else { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.check.fileinfo.nok'); ?></p>
+	<?php } ?>
+
 	<?php if ($res['data'] == 'ok') { ?>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t('gen.short.ok'); ?></span> <?php echo _t('install.check.data.ok'); ?></p>
 	<?php } else { ?>
@@ -634,7 +640,7 @@ 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}(:[0-9]{2,5})?" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" />
+				<input type="text" id="host" name="host" pattern="[0-9A-Z/a-z_.-]{1,64}(:[0-9]{2,5})?" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" />
 			</div>
 		</div>
 

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

@@ -20,7 +20,7 @@
 		<div>
 			<label class="checkbox" for="keep_logged_in">
 				<input type="checkbox" name="keep_logged_in" id="keep_logged_in" value="1" />
-				<?php echo _t('gen.auth.keep_logged_in'); ?>
+				<?php echo _t('gen.auth.keep_logged_in', $this->cookie_days); ?>
 			</label>
 			<br />
 		</div>

+ 60 - 41
app/views/helpers/export/articles.phtml

@@ -1,47 +1,66 @@
 <?php
-    $username = Minz_Session::param('currentUser', '_');
+$username = Minz_Session::param('currentUser', '_');
 
-    $articles = array(
-        'id' => 'user/' . str_replace('/', '', $username) . '/state/org.freshrss/' . $this->type,
-        'title' => $this->list_title,
-        'author' => $username,
-        'items' => array()
-    );
+$options = 0;
+if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
+	$options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
+}
 
-    foreach ($this->entries as $entry) {
-        if (!isset($this->feed)) {
-            $feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed ());
-        } else {
-            $feed = $this->feed;
-        }
+$articles = array(
+	'id' => 'user/' . str_replace('/', '', $username) . '/state/org.freshrss/' . $this->type,
+	'title' => $this->list_title,
+	'author' => $username,
+	'items' => array(),
+);
 
-        $articles['items'][] = array(
-            'id' => $entry->guid(),
-            'categories' => array_values($entry->tags()),
-            'title' => $entry->title(),
-            'author' => $entry->author(),
-            'published' => $entry->date(true),
-            'updated' => $entry->date(true),
-            'alternate' => array(array(
-                'href' => $entry->link(),
-                'type' => 'text/html'
-            )),
-            'content' => array(
-                'content' => $entry->content()
-            ),
-            'origin' => array(
-                'streamId' => $feed->id(),
-                'title' => $feed->name(),
-                'htmlUrl' => $feed->website(),
-                'feedUrl' => $feed->url()
-            )
-        );
-    }
+echo rtrim(json_encode($articles, $options), " ]}\n\r\t"), "\n";
+$first = true;
 
-    $options = 0;
-    if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
-        $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
-    }
+foreach ($this->entriesRaw as $entryRaw) {
+	if (empty($entryRaw)) {
+		continue;
+	}
+	$entry = FreshRSS_EntryDAO::daoToEntry($entryRaw);
+	if (!isset($this->feed)) {
+		$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed());
+		if ($feed == null) {
+			$feed = $entry->feed(true);
+		}
+	} else {
+		$feed = $this->feed;
+	}
 
-    echo json_encode($articles, $options);
-?>
+	$article = array(
+		'id' => $entry->guid(),
+		'categories' => array_values($entry->tags()),
+		'title' => $entry->title(),
+		'author' => $entry->author(),
+		'published' => $entry->date(true),
+		'updated' => $entry->date(true),
+		'alternate' => array(array(
+			'href' => $entry->link(),
+			'type' => 'text/html',
+		)),
+		'content' => array(
+			'content' => $entry->content(),
+		),
+		'origin' => array(
+			'streamId' => $feed == null ? '' : $feed->id(),
+			'title' => $feed == null ? '' : $feed->name(),
+			'htmlUrl' => $feed == null ? '' : $feed->website(),
+			'feedUrl' => $feed == null ? '' : $feed->url(),
+		)
+	);
+
+	$line = json_encode($article, $options);
+	if ($line != '') {
+		if ($first) {
+			$first = false;
+		} else {
+			echo ",\n";
+		}
+		echo $line;
+	}
+}
+
+echo "\n]}\n";

+ 25 - 3
cli/README.md

@@ -32,11 +32,11 @@ Options in parenthesis are optional.
 ```sh
 cd /usr/share/FreshRSS
 
-./cli/do-install.php --default_user admin --auth_type form  ( --environment production --base_url https://rss.example.net/ --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
-# --auth_type can be: 'form' (recommended), 'http_auth' (using the Web server access control), 'none' (dangerous)
+./cli/do-install.php --default_user admin ( --auth_type form --environment production --base_url https://rss.example.net/ --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
+# --auth_type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous)
 # --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)
 # --environment can be: 'production' (default), 'development' (for additional log messages)
-# --db-prefix is an optional prefix in front of the names of the tables
+# --db-prefix is an optional prefix in front of the names of the tables. We suggest using 'freshrss_'
 # This command does not create the default user. Do that with ./cli/create-user.php
 
 ./cli/create-user.php --user username ( --password 'password' --api-password 'api_password' --language en --email user@example.net --token 'longRandomString' --no-default-feeds )
@@ -55,4 +55,26 @@ cd /usr/share/FreshRSS
 ./cli/export-opml-for-user.php --user username > /path/to/file.opml.xml
 
 ./cli/export-zip-for-user.php --user username ( --max-feed-entries 100 ) > /path/to/file.zip
+
+./cli/user-info.php -h --user username
+# -h is to use a human-readable format
+# --user can be a username, or '*' to loop on all users
+# Returns a * if the user is admin, the name of the user, the date/time of last action, and the size occupied
+```
+
+
+## Unix piping
+
+It is possible to invoke a command multiple times, e.g. with different usernames, thanks to the `xargs -n1` command.
+
+Example showing user information for all users which username starts with 'a':
+
+```sh
+./cli/list-users.php | grep '^a' | xargs -n1 ./cli/user-info.php -h --user
+```
+
+Example showing all users ranked by date of last activity:
+
+```sh
+./cli/user-info.php -h --user '*' | sort -k2 -r
 ```

+ 3 - 0
cli/create-user.php

@@ -43,6 +43,9 @@ if (!$ok) {
 
 invalidateHttpCache(FreshRSS_Context::$system_conf->default_user);
 
+echo '• Remember to refresh the feeds of the user: ', $username , "\n",
+	"\t", './cli/actualize-user.php --user ', $username, "\n";
+
 accessRights();
 
 done($ok);

+ 7 - 7
cli/do-install.php

@@ -26,12 +26,12 @@ $dBparams = array(
 
 $options = getopt('', array_merge($params, $dBparams));
 
-if (empty($options['default_user']) || empty($options['auth_type'])) {
-	fail('Usage: ' . basename(__FILE__) . " --default_user admin --auth_type form" .
-		" ( --environment production --base_url https://rss.example.net/" .
+if (empty($options['default_user'])) {
+	fail('Usage: ' . basename(__FILE__) . " --default_user admin ( --auth_type form" .
+		" --environment production --base_url https://rss.example.net/" .
 		" --title FreshRSS --allow_anonymous --api_enabled" .
 		" --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
-		" --db-base freshrss --db-prefix freshrss )");
+		" --db-base freshrss --db-prefix freshrss_ )");
 }
 
 fwrite(STDERR, 'FreshRSS install…' . "\n");
@@ -51,7 +51,7 @@ if (!ctype_alnum($options['default_user'])) {
 	fail('FreshRSS invalid default username (must be ASCII alphanumeric): ' . $options['default_user']);
 }
 
-if (!in_array($options['auth_type'], array('form', 'http_auth', 'none'))) {
+if (isset($options['auth_type']) && !in_array($options['auth_type'], array('form', 'http_auth', 'none'))) {
 	fail('FreshRSS invalid authentication method (auth_type must be one of { form, http_auth, none }: ' . $options['auth_type']);
 }
 
@@ -86,11 +86,11 @@ if (file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . va
 $config['db']['default_user'] = $config['default_user'];
 if (!checkDb($config['db'])) {
 	@unlink(join_path(DATA_PATH, 'config.php'));
-	fail('FreshRSS database error: ' . (empty($config['db']['bd_error']) ? 'Unknown error' : $config['db']['bd_error']));
+	fail('FreshRSS database error: ' . (empty($config['db']['error']) ? 'Unknown error' : $config['db']['error']));
 }
 
 echo '• Remember to create the default user: ', $config['default_user'] , "\n",
-	"\t", './cli/create-user.php --user ', $config['default_user'] , " --password 'password' --more-options\n";
+	"\t", './cli/create-user.php --user ', $config['default_user'], " --password 'password' --more-options\n";
 
 accessRights();
 

+ 35 - 0
cli/user-info.php

@@ -0,0 +1,35 @@
+#!/usr/bin/php
+<?php
+require('_cli.php');
+
+$options = getopt('h', array(
+		'user:',
+	));
+
+if (empty($options['user'])) {
+	fail('Usage: ' . basename(__FILE__) . " -h --user username");
+}
+
+$users = $options['user'] === '*' ? listUsers() : array($options['user']);
+
+foreach ($users as $username) {
+	$username = cliInitUser($username);
+
+	$entryDAO = FreshRSS_Factory::createEntryDao($username);
+
+	echo $username === FreshRSS_Context::$system_conf->default_user ? '*' : ' ', "\t";
+
+	if (isset($options['h'])) {	//Human format
+		echo
+			$username, "\t",
+			date('c', FreshRSS_UserDAO::mtime($username)), "\t",
+			format_bytes($entryDAO->size()), "\t",
+			"\n";
+	} else {
+		echo
+			$username, "\t",
+			FreshRSS_UserDAO::mtime($username), "\t",
+			$entryDAO->size(), "\t",
+			"\n";
+	}
+}

+ 1 - 1
constants.php

@@ -1,5 +1,5 @@
 <?php
-define('FRESHRSS_VERSION', '1.6.1');
+define('FRESHRSS_VERSION', '1.6.2');
 define('FRESHRSS_WEBSITE', 'http://freshrss.org');
 define('FRESHRSS_WIKI', 'http://doc.freshrss.org');
 

+ 4 - 1
data/config.default.php

@@ -46,7 +46,7 @@ return array(
 	#	`http_auth` is an access controled by the HTTP Web server (e.g. `/FreshRSS/p/i/.htaccess` for Apache)
 	#		if you use `http_auth`, remember to protect only `/FreshRSS/p/i/`,
 	#		and in particular not protect `/FreshRSS/p/api/` if you would like to use the API (different login system).
-	'auth_type' => 'none',
+	'auth_type' => 'form',
 
 	# Allow or not the use of the API, used for mobile apps.
 	#	End-point is http://example.net/FreshRSS/p/api/greader.php
@@ -74,6 +74,9 @@ return array(
 
 	'limits' => array(
 
+		# Duration in seconds of the login cookie.
+		'cookie_duration' => 2592000,
+
 		# Duration in seconds of the SimplePie cache,
 		#	during which a query to the RSS feed will return the local cached version.
 		# Especially important for multi-user setups.

+ 3 - 1
extensions/README.md

@@ -1,3 +1,5 @@
 # FreshRSS extensions
 
-You may place in this directory some custom extensions for FreshRSS.
+You may place custom extensions for FreshRSS in this directory.
+
+You can find some extensions in our [GitHub repository](https://github.com/FreshRSS/Extensions).

+ 6 - 0
lib/Minz/ModelPdo.php

@@ -116,6 +116,12 @@ class Minz_ModelPdo {
 		self::$sharedBd = null;
 		self::$sharedPrefix = '';
 	}
+
+	public function disableBuffering() {
+		if ((self::$sharedDbType === 'mysql') && defined('PDO::MYSQL_ATTR_USE_BUFFERED_QUERY')) {
+			$this->bd->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
+		}
+	}
 }
 
 class MinzPDO extends PDO {

+ 5 - 2
lib/favicons.php

@@ -9,7 +9,7 @@ $default_favicon = PUBLIC_PATH . '/themes/icons/default_favicon.ico';
 function download_favicon($website, $dest) {
 	global $favicons_dir, $default_favicon;
 
-	syslog(LOG_DEBUG, 'FreshRSS Favicon discovery GET ' . $website);
+	syslog(LOG_INFO, 'FreshRSS Favicon discovery GET ' . $website);
 	$favicon_getter = new \Favicon\Favicon();
 	$favicon_getter->setCacheDir($favicons_dir);
 	$favicon_url = $favicon_getter->get($website);
@@ -18,11 +18,12 @@ function download_favicon($website, $dest) {
 		return @copy($default_favicon, $dest);
 	}
 
-	syslog(LOG_DEBUG, 'FreshRSS Favicon GET ' . $favicon_url);
+	syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $favicon_url);
 	$c = curl_init($favicon_url);
 	curl_setopt($c, CURLOPT_HEADER, false);
 	curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
 	curl_setopt($c, CURLOPT_BINARYTRANSFER, true);
+	curl_setopt($c, CURLOPT_USERAGENT, 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')');
 	$img_raw = curl_exec($c);
 	$status_code = curl_getinfo($c, CURLINFO_HTTP_CODE);
 	curl_close($c);
@@ -34,6 +35,8 @@ function download_favicon($website, $dest) {
 			fclose($file);
 			return true;
 		}
+	} else {
+		syslog(LOG_WARNING, 'FreshRSS Favicon GET ' . $favicon_url . ' error ' . $status_code);
 	}
 
 	return false;

+ 8 - 2
lib/lib_install.php

@@ -15,6 +15,7 @@ function checkRequirements() {
 	$pdo = $pdo_mysql || $pdo_sqlite || $pdo_pgsql;
 	$pcre = extension_loaded('pcre');
 	$ctype = extension_loaded('ctype');
+	$fileinfo = extension_loaded('fileinfo');
 	$dom = class_exists('DOMDocument');
 	$xml = function_exists('xml_parser_create');
 	$json = function_exists('json_encode');
@@ -34,6 +35,7 @@ function checkRequirements() {
 		'pdo' => $pdo ? 'ok' : 'ko',
 		'pcre' => $pcre ? 'ok' : 'ko',
 		'ctype' => $ctype ? 'ok' : 'ko',
+		'fileinfo' => $fileinfo ? 'ok' : 'ko',
 		'dom' => $dom ? 'ok' : 'ko',
 		'xml' => $xml ? 'ok' : 'ko',
 		'json' => $json ? 'ok' : 'ko',
@@ -42,7 +44,7 @@ function checkRequirements() {
 		'users' => $users ? 'ok' : 'ko',
 		'favicons' => $favicons ? 'ok' : 'ko',
 		'http_referer' => $http_referer ? 'ok' : 'ko',
-		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $xml &&
+		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $fileinfo && $dom && $xml &&
 		         $data && $cache && $users && $favicons && $http_referer ?
 		         'ok' : 'ko'
 	);
@@ -54,8 +56,8 @@ function generateSalt() {
 
 function checkDb(&$dbOptions) {
 	$dsn = '';
+	$driver_options = null;
 	try {
-		$driver_options = null;
 		switch ($dbOptions['type']) {
 		case 'mysql':
 			include_once(APP_PATH . '/SQL/install.sql.mysql.php');
@@ -99,8 +101,12 @@ function checkDb(&$dbOptions) {
 		default:
 			return false;
 		}
+
+		$c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options);
+		$res = $c->query('SELECT 1');
 	} catch (PDOException $e) {
 		$dsn = '';
+		syslog(LOG_DEBUG, 'FreshRSS SQL warning: ' . $e->getMessage());
 		$dbOptions['error'] = $e->getMessage();
 	}
 	$dbOptions['dsn'] = $dsn;

+ 3 - 0
lib/lib_rss.php

@@ -127,6 +127,8 @@ function format_bytes($bytes, $precision = 2, $system = 'IEC') {
 	} elseif ($system === 'SI') {
 		$base = 1000;
 		$units = array('B', 'KB', 'MB', 'GB', 'TB');
+	} else {
+		return format_number($bytes, $precision);
 	}
 	$bytes = max(intval($bytes), 0);
 	$pow = $bytes === 0 ? 0 : floor(log($bytes) / log($base));
@@ -396,6 +398,7 @@ function check_install_php() {
 		'pdo' => $pdo_mysql || $pdo_sqlite,
 		'pcre' => extension_loaded('pcre'),
 		'ctype' => extension_loaded('ctype'),
+		'fileinfo' => extension_loaded('fileinfo'),
 		'dom' => class_exists('DOMDocument'),
 		'json' => extension_loaded('json'),
 		'zip' => extension_loaded('zip'),

+ 1 - 1
p/scripts/main.js

@@ -1354,7 +1354,6 @@ function init_beforeDOM() {
 		window.setTimeout(init_beforeDOM, 100);
 		return;
 	}
-	init_confirm_action();
 	if (['normal', 'reader', 'global'].indexOf(context.current_view) >= 0) {
 		inject_script('jquery.sticky-kit.min.js');
 		init_normal();
@@ -1372,6 +1371,7 @@ function init_afterDOM() {
 	init_notifications();
 	$stream = $('#stream');
 	if ($stream.length > 0) {
+		init_confirm_action();
 		init_load_more($stream);
 		init_posts();
 		init_nav_entries();