فهرست منبع

Merge branch 'dev' into cli-act-num-articles

Seokseong Jeon 9 سال پیش
والد
کامیت
cb897980cd
83فایلهای تغییر یافته به همراه1458 افزوده شده و 928 حذف شده
  1. 27 0
      CHANGELOG.md
  2. 1 0
      CREDITS.md
  3. 7 3
      README.fr.md
  4. 8 4
      README.md
  5. 2 7
      app/Controllers/authController.php
  6. 32 4
      app/Controllers/feedController.php
  7. 9 5
      app/Controllers/importExportController.php
  8. 1 0
      app/Controllers/updateController.php
  9. 6 1
      app/Controllers/userController.php
  10. 23 4
      app/Models/Auth.php
  11. 7 1
      app/Models/Entry.php
  12. 139 50
      app/Models/EntryDAO.php
  13. 26 0
      app/Models/EntryDAOPGSQL.php
  14. 30 9
      app/Models/EntryDAOSQLite.php
  15. 1 8
      app/Models/Factory.php
  16. 8 7
      app/Models/Feed.php
  17. 26 30
      app/Models/FeedDAO.php
  18. 0 19
      app/Models/FeedDAOSQLite.php
  19. 139 29
      app/Models/Search.php
  20. 6 4
      app/Models/UserDAO.php
  21. 27 1
      app/SQL/install.sql.mysql.php
  22. 26 2
      app/SQL/install.sql.pgsql.php
  23. 26 3
      app/SQL/install.sql.sqlite.php
  24. 10 10
      app/i18n/en/admin.php
  25. 18 18
      app/i18n/en/conf.php
  26. 5 5
      app/i18n/en/feedback.php
  27. 1 1
      app/i18n/en/gen.php
  28. 2 2
      app/i18n/en/index.php
  29. 3 3
      app/i18n/en/sub.php
  30. 12 28
      app/install.php
  31. 1 0
      app/layout/nav_menu.phtml
  32. 0 13
      app/views/auth/index.phtml
  33. 13 0
      app/views/user/profile.phtml
  34. 1 1
      constants.php
  35. 0 41
      lib/Favicon/DataAccess.php
  36. 0 296
      lib/Favicon/Favicon.php
  37. 51 35
      lib/SimplePie/SimplePie.php
  38. 2 3
      lib/SimplePie/SimplePie/Author.php
  39. 6 5
      lib/SimplePie/SimplePie/Cache.php
  40. 2 3
      lib/SimplePie/SimplePie/Cache/Base.php
  41. 2 3
      lib/SimplePie/SimplePie/Cache/DB.php
  42. 2 3
      lib/SimplePie/SimplePie/Cache/File.php
  43. 2 3
      lib/SimplePie/SimplePie/Cache/Memcache.php
  44. 166 0
      lib/SimplePie/SimplePie/Cache/Memcached.php
  45. 19 4
      lib/SimplePie/SimplePie/Cache/MySQL.php
  46. 166 0
      lib/SimplePie/SimplePie/Cache/Redis.php
  47. 2 3
      lib/SimplePie/SimplePie/Caption.php
  48. 2 3
      lib/SimplePie/SimplePie/Category.php
  49. 2 3
      lib/SimplePie/SimplePie/Content/Type/Sniffer.php
  50. 2 3
      lib/SimplePie/SimplePie/Copyright.php
  51. 2 3
      lib/SimplePie/SimplePie/Core.php
  52. 2 3
      lib/SimplePie/SimplePie/Credit.php
  53. 2 3
      lib/SimplePie/SimplePie/Decode/HTML/Entities.php
  54. 3 4
      lib/SimplePie/SimplePie/Enclosure.php
  55. 2 3
      lib/SimplePie/SimplePie/Exception.php
  56. 8 8
      lib/SimplePie/SimplePie/File.php
  57. 2 3
      lib/SimplePie/SimplePie/HTTP/Parser.php
  58. 43 24
      lib/SimplePie/SimplePie/IRI.php
  59. 12 25
      lib/SimplePie/SimplePie/Item.php
  60. 55 5
      lib/SimplePie/SimplePie/Locator.php
  61. 5 5
      lib/SimplePie/SimplePie/Misc.php
  62. 2 3
      lib/SimplePie/SimplePie/Net/IPv6.php
  63. 5 6
      lib/SimplePie/SimplePie/Parse/Date.php
  64. 2 3
      lib/SimplePie/SimplePie/Parser.php
  65. 2 3
      lib/SimplePie/SimplePie/Rating.php
  66. 2 3
      lib/SimplePie/SimplePie/Registry.php
  67. 2 3
      lib/SimplePie/SimplePie/Restriction.php
  68. 3 3
      lib/SimplePie/SimplePie/Sanitize.php
  69. 2 3
      lib/SimplePie/SimplePie/Source.php
  70. 2 3
      lib/SimplePie/SimplePie/XML/Declaration/Parser.php
  71. 2 3
      lib/SimplePie/SimplePie/gzdecode.php
  72. 98 32
      lib/favicons.php
  73. 1 1
      lib/lib_install.php
  74. 3 0
      lib/lib_rss.php
  75. 30 9
      p/api/pshb.php
  76. 1 2
      p/f.php
  77. 11 3
      p/scripts/main.js
  78. 1 1
      p/themes/base-theme/template.css
  79. 7 0
      tests/README.md
  80. 1 1
      tests/app/Models/CategoryTest.php
  81. 0 5
      tests/app/Models/ContextTest.php
  82. 75 74
      tests/app/Models/SearchTest.php
  83. 1 1
      tests/app/Models/UserQueryTest.php

+ 27 - 0
CHANGELOG.md

@@ -1,5 +1,32 @@
 # Changelog
 # Changelog
 
 
+## 2017-xx-xx FreshRSS 1.7.0-dev
+* Features:
+	* Deferred insertion of new articles, for better chronological order [#530](https://github.com/FreshRSS/FreshRSS/issues/530)
+	* Better search:
+		* Possibility to use multiple `intitle:`, `inurl:`, `author:` [#1478](https://github.com/FreshRSS/FreshRSS/pull/1478)
+		* Negative searches with `!` or `-` [#1381](https://github.com/FreshRSS/FreshRSS/issues/1381)
+			* Examples: `!intitle:unwanted`, `-intitle:unwanted`, `-inurl:unwanted`, `-author:unwanted`, `-#unwanted`, `-unwanted`
+		* Allow double-quotes, such as `author:"some name"`, in addition to single-quotes such as `author:'some name'` [#1478](https://github.com/FreshRSS/FreshRSS/pull/1478)
+	* Multi-user tokens (to access RSS outputs of any user) [#1390](https://github.com/FreshRSS/FreshRSS/issues/1390)
+* Compatibility:
+	* Add support for PHP 7.1 [#1471](https://github.com/FreshRSS/FreshRSS/issues/1471)
+	* PostgreSQL is not experimental anymore [#1476](https://github.com/FreshRSS/FreshRSS/pull/1476)
+* Bug fixing
+	* Fix PubSubHubbub bugs when deleting users, and improved behaviour when removing feeds [#1495](https://github.com/FreshRSS/FreshRSS/pull/1495)
+	* Fix SQL uniqueness bug with PostgreSQL [#1476](https://github.com/FreshRSS/FreshRSS/pull/1476)
+		* (Require manual update for existing installations)
+	* Do not require PHP extension `fileinfo` for favicons [#1461](https://github.com/FreshRSS/FreshRSS/issues/1461)
+	* Fix UI lowest subscription popup hidden [#1479](https://github.com/FreshRSS/FreshRSS/issues/1479)
+	* Fix update system via ZIP archive [#1498](https://github.com/FreshRSS/FreshRSS/pull/1498)
+	* Work around for IE / Edge bug in username pattern in version 1.6.3 [#1511](https://github.com/FreshRSS/FreshRSS/issues/1511)
+* I18n
+	* Improve English [#1465](https://github.com/FreshRSS/FreshRSS/pull/1465)
+* Misc.
+	* Fall back to article URL when the article GUID is empty [#1482](https://github.com/FreshRSS/FreshRSS/issues/1482)
+	* Rewritten Favicon library using cURL [#1504](https://github.com/FreshRSS/FreshRSS/pull/1504)
+
+
 ## 2017-03-11 FreshRSS 1.6.3
 ## 2017-03-11 FreshRSS 1.6.3
 
 
 * Features
 * Features

+ 1 - 0
CREDITS.md

@@ -21,6 +21,7 @@ People are sorted by name so please keep this order.
 * [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/)
 * [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/)
 * [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb)
 * [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/)
 * [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=edhelas), [Web](http://edhelas.movim.eu/)
+* [jlefler](https://github.com/jlefler): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jlefler)
 * [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/)
 * [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/)
 * [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/)
 * [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ldidry), [Web](https://www.fiat-tux.fr/)

+ 7 - 3
README.fr.md

@@ -35,12 +35,16 @@ Nous sommes une communauté amicale.
 * PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances)
 * PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances)
 	* Requis : [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), et [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql)
 	* Requis : [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), et [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql)
 	* Recommandés : [JSON](http://php.net/json), [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), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [ZIP](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
 	* Recommandés : [JSON](http://php.net/json), [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), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [ZIP](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
-* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL (experimental)
+* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL 9.2+
 * Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 * Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 	* Fonctionne aussi sur mobile
 	* Fonctionne aussi sur mobile
 
 
 ![Capture d’écran de FreshRSS](./doc/FreshRSS-screenshot.png)
 ![Capture d’écran de FreshRSS](./doc/FreshRSS-screenshot.png)
 
 
+# Documentation
+* http://doc.freshrss.org/fr/
+* https://github.com/FreshRSS/documentation
+
 # Installation
 # Installation
 1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](../releases)
 1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](../releases)
 2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
 2. Placez l’application sur votre serveur (la partie à exposer au Web est le répertoire `./p/`)
@@ -130,7 +134,8 @@ Créer `/etc/cron.d/FreshRSS` avec :
 * Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
 * Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
 	* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
 	* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
 * Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici.
 * Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici.
-* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
+* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/users/*/log*.txt`.
+	* Le répertoire spécial `./data/users/_/` contient la partie des logs partagés par tous les utilisateurs.
 
 
 
 
 # Sauvegarde
 # Sauvegarde
@@ -154,7 +159,6 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
 * [MINZ](https://github.com/marienfressinaud/MINZ)
 * [MINZ](https://github.com/marienfressinaud/MINZ)
 * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
 * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
 * [jQuery](http://jquery.com/)
 * [jQuery](http://jquery.com/)
-* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/)
 * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/)
 * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
 * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)

+ 8 - 4
README.md

@@ -35,12 +35,16 @@ We are a friendly community.
 * PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance)
 * PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance)
 	* Required extensions: [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), and [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql)
 	* Required extensions: [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), and [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql)
 	* Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [ZIP](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
 	* Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [ZIP](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
-* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL (experimental)
+* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL 9.2+
 * A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 * A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 	* Works on mobile
 	* Works on mobile
 
 
 ![FreshRSS screenshot](./doc/FreshRSS-screenshot.png)
 ![FreshRSS screenshot](./doc/FreshRSS-screenshot.png)
 
 
+# Documentation
+* http://doc.freshrss.org/en/
+* https://github.com/FreshRSS/documentation
+
 # Installation
 # Installation
 1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip)
 1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip)
 2. Dump the application on your server (expose only the `./p/` folder)
 2. Dump the application on your server (expose only the `./p/` folder)
@@ -128,10 +132,11 @@ Create `/etc/cron.d/FreshRSS` with:
 
 
 
 
 # Advices
 # Advices
-* For a better security, expose only the `./p/` folder on the web.
+* For a better security, expose only the `./p/` folder on the Web.
 	* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
 	* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
 * The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here.
 * The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here.
-* If you encounter any problem, logs are accessible from the interface or manually in `./data/log/*.log` files.
+* If you encounter any problem, logs are accessible from the interface or manually in `./data/users/*/log*.txt` files.
+	* The special folder `./data/users/_/` contains the part of the logs that are shared by all users.
 
 
 
 
 # Backup
 # Backup
@@ -155,7 +160,6 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
 * [MINZ](https://github.com/marienfressinaud/MINZ)
 * [MINZ](https://github.com/marienfressinaud/MINZ)
 * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
 * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/)
 * [jQuery](http://jquery.com/)
 * [jQuery](http://jquery.com/)
-* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [lib_opml](https://github.com/marienfressinaud/lib_opml)
 * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/)
 * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/)
 * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)
 * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/)

+ 2 - 7
app/Controllers/authController.php

@@ -27,11 +27,6 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
 			$ok = true;
 			$ok = true;
 
 
-			$current_token = FreshRSS_Context::$user_conf->token;
-			$token = Minz_Request::param('token', $current_token);
-			FreshRSS_Context::$user_conf->token = $token;
-			$ok &= FreshRSS_Context::$user_conf->save();
-
 			$anon = Minz_Request::param('anon_access', false);
 			$anon = Minz_Request::param('anon_access', false);
 			$anon = ((bool)$anon) && ($anon !== 'no');
 			$anon = ((bool)$anon) && ($anon !== 'no');
 			$anon_refresh = Minz_Request::param('anon_refresh', false);
 			$anon_refresh = Minz_Request::param('anon_refresh', false);
@@ -123,7 +118,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 			$challenge = Minz_Request::param('challenge', '');
 			$challenge = Minz_Request::param('challenge', '');
 
 
 			$conf = get_user_configuration($username);
 			$conf = get_user_configuration($username);
-			if (is_null($conf)) {
+			if ($conf == null) {
 				Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
 				Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
 				return;
 				return;
 			}
 			}
@@ -164,7 +159,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 			}
 			}
 
 
 			$conf = get_user_configuration($username);
 			$conf = get_user_configuration($username);
-			if (is_null($conf)) {
+			if ($conf == null) {
 				return;
 				return;
 			}
 			}
 
 

+ 32 - 4
app/Controllers/feedController.php

@@ -226,7 +226,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
-	public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) {
+	public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false, $noCommit = false) {
 		@set_time_limit(300);
 		@set_time_limit(300);
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -309,6 +309,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				// -2 means we take the default value from configuration
 				// -2 means we take the default value from configuration
 				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
 				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
 			}
 			}
+			$needFeedCacheRefresh = false;
 
 
 			// We want chronological order and SimplePie uses reverse order.
 			// We want chronological order and SimplePie uses reverse order.
 			$entries = array_reverse($feed->entries());
 			$entries = array_reverse($feed->entries());
@@ -334,6 +335,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 							//Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() .
 							//Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() .
 								//', old hash ' . $existingHash . ', new hash ' . $entry->hash());
 								//', old hash ' . $existingHash . ', new hash ' . $entry->hash());
 							//TODO: Make an updated/is_read policy by feed, in addition to the global one.
 							//TODO: Make an updated/is_read policy by feed, in addition to the global one.
+							$needFeedCacheRefresh = FreshRSS_Context::$user_conf->mark_updated_article_unread;
 							$entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null);	//Change is_read according to policy.
 							$entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null);	//Change is_read according to policy.
 							if (!$entryDAO->inTransaction()) {
 							if (!$entryDAO->inTransaction()) {
 								$entryDAO->beginTransaction();
 								$entryDAO->beginTransaction();
@@ -390,12 +392,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				                                $date_min,
 				                                $date_min,
 				                                max($feed_history, count($entries) + 10));
 				                                max($feed_history, count($entries) + 10));
 				if ($nb > 0) {
 				if ($nb > 0) {
+					$needFeedCacheRefresh = true;
 					Minz_Log::debug($nb . ' old entries cleaned in feed [' .
 					Minz_Log::debug($nb . ' old entries cleaned in feed [' .
 					                $feed->url() . ']');
 					                $feed->url() . ']');
 				}
 				}
 			}
 			}
 
 
-			$feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime);
+			$feedDAO->updateLastUpdate($feed->id(), false, $mtime);
+			if ($needFeedCacheRefresh) {
+				$feedDAO->updateCachedValue($feed->id());
+			}
 			if ($entryDAO->inTransaction()) {
 			if ($entryDAO->inTransaction()) {
 				$entryDAO->commit();
 				$entryDAO->commit();
 			}
 			}
@@ -436,6 +442,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				break;
 				break;
 			}
 			}
 		}
 		}
+		if (!$noCommit) {
+			if (!$entryDAO->inTransaction()) {
+				$entryDAO->beginTransaction();
+			}
+			$entryDAO->commitNewEntries();
+			$feedDAO->updateCachedValues();
+			if ($entryDAO->inTransaction()) {
+				$entryDAO->commit();
+			}
+		}
 		return array($updated_feeds, reset($feeds), $nb_new_articles);
 		return array($updated_feeds, reset($feeds), $nb_new_articles);
 	}
 	}
 
 
@@ -446,6 +462,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 	 *   - id (default: false): Feed ID
 	 *   - id (default: false): Feed ID
 	 *   - url (default: false): Feed URL
 	 *   - url (default: false): Feed URL
 	 *   - force (default: false)
 	 *   - force (default: false)
+	 *   - noCommit (default: 0): Set to 1 to prevent committing the new articles to the main database
 	 * If id and url are not specified, all the feeds are actualized. But if force is
 	 * If id and url are not specified, all the feeds are actualized. But if force is
 	 * false, process stops at 10 feeds to avoid time execution problem.
 	 * false, process stops at 10 feeds to avoid time execution problem.
 	 */
 	 */
@@ -454,8 +471,19 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$id = Minz_Request::param('id');
 		$id = Minz_Request::param('id');
 		$url = Minz_Request::param('url');
 		$url = Minz_Request::param('url');
 		$force = Minz_Request::param('force');
 		$force = Minz_Request::param('force');
-
-		list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force);
+		$noCommit = Minz_Request::fetchPOST('noCommit', 0) == 1;
+
+		if ($id == -1 && !$noCommit) {	//Special request only to commit & refresh DB cache
+			$updated_feeds = 0;
+			$entryDAO = FreshRSS_Factory::createEntryDao();
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$entryDAO->beginTransaction();
+			$entryDAO->commitNewEntries();
+			$feedDAO->updateCachedValues();
+			$entryDAO->commit();
+		} else {
+			list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force, null, false, $noCommit);
+		}
 
 
 		if (Minz_Request::param('ajax')) {
 		if (Minz_Request::param('ajax')) {
 			// Most of the time, ajax request is for only one feed. But since
 			// Most of the time, ajax request is for only one feed. But since

+ 9 - 5
app/Controllers/importExportController.php

@@ -464,18 +464,22 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			}
 			}
 
 
 			$values = $entry->toArray();
 			$values = $entry->toArray();
+			$ok = false;
 			if (isset($existingHashForGuids[$entry->guid()])) {
 			if (isset($existingHashForGuids[$entry->guid()])) {
-				$id = $this->entryDAO->updateEntry($values);
+				$ok = $this->entryDAO->updateEntry($values);
 			} else {
 			} else {
-				$id = $this->entryDAO->addEntry($values);
+				$ok = $this->entryDAO->addEntry($values);
 			}
 			}
+			$error |= ($ok === false);
 
 
-			if (!$error && ($id === false)) {
-				$error = true;
-			}
 		}
 		}
 		$this->entryDAO->commit();
 		$this->entryDAO->commit();
 
 
+		$this->entryDAO->beginTransaction();
+		$this->entryDAO->commitNewEntries();
+		$this->feedDAO->updateCachedValues();
+		$this->entryDAO->commit();
+
 		return !$error;
 		return !$error;
 	}
 	}
 
 

+ 1 - 0
app/Controllers/updateController.php

@@ -190,6 +190,7 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 			if (self::isGit()) {
 			if (self::isGit()) {
 				$res = self::gitPull();
 				$res = self::gitPull();
 			} else {
 			} else {
+				require(UPDATE_FILENAME);
 				if (Minz_Request::isPost()) {
 				if (Minz_Request::isPost()) {
 					save_info_update();
 					save_info_update();
 				}
 				}

+ 6 - 1
app/Controllers/userController.php

@@ -38,7 +38,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * The username is also used as folder name, file name, and part of SQL table name.
 	 * The username is also used as folder name, file name, and part of SQL table name.
 	 * '_' is a reserved internal username.
 	 * '_' is a reserved internal username.
 	 */
 	 */
-	const USERNAME_PATTERN = '[0-9a-zA-Z]|[0-9a-zA-Z_]{2,38}';
+	const USERNAME_PATTERN = '[0-9a-zA-Z_]{2,38}|[0-9a-zA-Z]';
 
 
 	public static function checkUsername($username) {
 	public static function checkUsername($username) {
 		return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
 		return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
@@ -74,6 +74,10 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash;
 				FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash;
 			}
 			}
 
 
+			$current_token = FreshRSS_Context::$user_conf->token;
+			$token = Minz_Request::param('token', $current_token);
+			FreshRSS_Context::$user_conf->token = $token;
+
 			$ok &= FreshRSS_Context::$user_conf->save();
 			$ok &= FreshRSS_Context::$user_conf->save();
 
 
 			if ($ok) {
 			if ($ok) {
@@ -213,6 +217,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			$userDAO = new FreshRSS_UserDAO();
 			$userDAO = new FreshRSS_UserDAO();
 			$ok &= $userDAO->deleteUser($username);
 			$ok &= $userDAO->deleteUser($username);
 			$ok &= recursive_unlink($user_data);
 			$ok &= recursive_unlink($user_data);
+			array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt'));
 		}
 		}
 		return $ok;
 		return $ok;
 	}
 	}

+ 23 - 4
app/Models/Auth.php

@@ -74,6 +74,10 @@ class FreshRSS_Auth {
 	public static function giveAccess() {
 	public static function giveAccess() {
 		$current_user = Minz_Session::param('currentUser');
 		$current_user = Minz_Session::param('currentUser');
 		$user_conf = get_user_configuration($current_user);
 		$user_conf = get_user_configuration($current_user);
+		if ($user_conf == null) {
+			self::$login_ok = false;
+			return;
+		}
 		$system_conf = Minz_Configuration::get('system');
 		$system_conf = Minz_Configuration::get('system');
 
 
 		switch ($system_conf->auth_type) {
 		switch ($system_conf->auth_type) {
@@ -120,13 +124,28 @@ class FreshRSS_Auth {
 	 * Removes all accesses for the current user.
 	 * Removes all accesses for the current user.
 	 */
 	 */
 	public static function removeAccess() {
 	public static function removeAccess() {
-		Minz_Session::_param('loginOk');
 		self::$login_ok = false;
 		self::$login_ok = false;
-		$conf = Minz_Configuration::get('system');
-		Minz_Session::_param('currentUser', $conf->default_user);
+		Minz_Session::_param('loginOk');
 		Minz_Session::_param('csrf');
 		Minz_Session::_param('csrf');
+		$system_conf = Minz_Configuration::get('system');
 
 
-		switch ($conf->auth_type) {
+		$username = '';
+		$token_param = Minz_Request::param('token', '');
+		if ($token_param != '') {
+			$username = trim(Minz_Request::param('user', ''));
+			if ($username != '') {
+				$conf = get_user_configuration($username);
+				if ($conf == null) {
+					$username = '';
+				}
+			}
+		}
+		if ($username == '') {
+			$username = $system_conf->default_user;
+		}
+		Minz_Session::_param('currentUser', $username);
+
+		switch ($system_conf->auth_type) {
 		case 'form':
 		case 'form':
 			Minz_Session::_param('passwordHash');
 			Minz_Session::_param('passwordHash');
 			FreshRSS_FormAuth::deleteCookie();
 			FreshRSS_FormAuth::deleteCookie();

+ 7 - 1
app/Models/Entry.php

@@ -22,7 +22,6 @@ class FreshRSS_Entry extends Minz_Model {
 
 
 	public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '',
 	public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '',
 	                            $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
 	                            $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
-		$this->_guid($guid);
 		$this->_title($title);
 		$this->_title($title);
 		$this->_author($author);
 		$this->_author($author);
 		$this->_content($content);
 		$this->_content($content);
@@ -32,6 +31,7 @@ class FreshRSS_Entry extends Minz_Model {
 		$this->_isFavorite($is_favorite);
 		$this->_isFavorite($is_favorite);
 		$this->_feed($feed);
 		$this->_feed($feed);
 		$this->_tags(preg_split('/[\s#]/', $tags));
 		$this->_tags(preg_split('/[\s#]/', $tags));
+		$this->_guid($guid);
 	}
 	}
 
 
 	public function id() {
 	public function id() {
@@ -101,6 +101,12 @@ class FreshRSS_Entry extends Minz_Model {
 		$this->id = $value;
 		$this->id = $value;
 	}
 	}
 	public function _guid($value) {
 	public function _guid($value) {
+		if ($value == '') {
+			$value = $this->link;
+			if ($value == '') {
+				$value = $this->hash();
+			}
+		}
 		$this->guid = $value;
 		$this->guid = $value;
 	}
 	}
 	public function _title($value) {
 	public function _title($value) {

+ 139 - 50
app/Models/EntryDAO.php

@@ -88,6 +88,38 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return false;
 		return false;
 	}
 	}
 
 
+	protected function createEntryTempTable() {
+		$ok = false;
+		$hadTransaction = $this->bd->inTransaction();
+		if ($hadTransaction) {
+			$this->bd->commit();
+		}
+		try {
+			$db = FreshRSS_Context::$system_conf->db;
+			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+			Minz_Log::warning('SQL CREATE TABLE entrytmp...');
+			if (defined('SQL_CREATE_TABLE_ENTRYTMP')) {
+				$sql = sprintf(SQL_CREATE_TABLE_ENTRYTMP, $this->prefix);
+				$stm = $this->bd->prepare($sql);
+				$ok = $stm && $stm->execute();
+			} else {
+				global $SQL_CREATE_TABLE_ENTRYTMP;
+				$ok = !empty($SQL_CREATE_TABLE_ENTRYTMP);
+				foreach ($SQL_CREATE_TABLE_ENTRYTMP as $instruction) {
+					$sql = sprintf($instruction, $this->prefix);
+					$stm = $this->bd->prepare($sql);
+					$ok &= $stm && $stm->execute();
+				}
+			}
+		} catch (Exception $e) {
+			Minz_Log::error('FreshRSS_EntryDAO::createEntryTempTable error: ' . $e->getMessage());
+		}
+		if ($hadTransaction) {
+			$this->bd->beginTransaction();
+		}
+		return $ok;
+	}
+
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
 		if (isset($errorInfo[0])) {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === '42S22') {	//ER_BAD_FIELD_ERROR
 			if ($errorInfo[0] === '42S22') {	//ER_BAD_FIELD_ERROR
@@ -97,6 +129,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 						return $this->addColumn($column);
 						return $this->addColumn($column);
 					}
 					}
 				}
 				}
+			} elseif ($errorInfo[0] === '42S02' && stripos($errorInfo[2], 'entrytmp') !== false) {	//ER_BAD_TABLE_ERROR
+				return $this->createEntryTempTable();	//v1.7
 			}
 			}
 		}
 		}
 		if (isset($errorInfo[1])) {
 		if (isset($errorInfo[1])) {
@@ -110,8 +144,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	private $addEntryPrepared = null;
 	private $addEntryPrepared = null;
 
 
 	public function addEntry($valuesTmp) {
 	public function addEntry($valuesTmp) {
-		if ($this->addEntryPrepared === null) {
-			$sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, '
+		if ($this->addEntryPrepared == null) {
+			$sql = 'INSERT INTO `' . $this->prefix . 'entrytmp` (id, guid, title, author, '
 			     . ($this->isCompressed() ? 'content_bin' : 'content')
 			     . ($this->isCompressed() ? 'content_bin' : 'content')
 			     . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
 			     . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
 			     . 'VALUES(:id, :guid, :title, :author, '
 			     . 'VALUES(:id, :guid, :title, :author, '
@@ -121,43 +155,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			     . ', :is_read, :is_favorite, :id_feed, :tags)';
 			     . ', :is_read, :is_favorite, :id_feed, :tags)';
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 		}
 		}
-		$this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
-		$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
-		$valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
-		$this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
-		$valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
-		$this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
-		$valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
-		$this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
-		$this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
-		$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
-		$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
-		$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
-		$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
-		$valuesTmp['lastSeen'] = time();
-		$this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
-		$valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
-		$this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
-		$valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
-		$this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
-		$this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
-		$valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
-		$this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
-
-		if ($this->hasNativeHex()) {
-			$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
-		} else {
-			$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
-			$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
+		if ($this->addEntryPrepared) {
+			$this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
+			$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+			$valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
+			$this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+			$valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+			$this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
+			$valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+			$this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
+			$this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
+			$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+			$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
+			$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
+			$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+			$valuesTmp['lastSeen'] = time();
+			$this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
+			$valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
+			$this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
+			$valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
+			$this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
+			$this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+			$valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+			$this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+			if ($this->hasNativeHex()) {
+				$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+			} else {
+				$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
+				$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
+			}
 		}
 		}
-
 		if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
 		if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
-			return $this->bd->lastInsertId();
+			return true;
 		} else {
 		} else {
 			$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
 			$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
+				$this->addEntryPrepared = null;
 				return $this->addEntry($valuesTmp);
 				return $this->addEntry($valuesTmp);
-			} elseif ((int)($info[0] / 1000) !== 23) {	//Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
+			} elseif ((int)((int)$info[0] / 1000) !== 23) {	//Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
 				Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
 				Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
 					. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']);
 					. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']);
 			}
 			}
@@ -165,6 +201,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 	}
 	}
 
 
+	public function commitNewEntries() {
+		$sql = 'SET @rank=(SELECT MAX(id) - COUNT(*) FROM `' . $this->prefix . 'entrytmp`); ' .	//MySQL-specific
+			'INSERT IGNORE INTO `' . $this->prefix . 'entry` (id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) ' .
+				'SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date; ' .
+			'DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= @rank;';
+		$hadTransaction = $this->bd->inTransaction();
+		if (!$hadTransaction) {
+			$this->bd->beginTransaction();
+		}
+		$result = $this->bd->exec($sql) !== false;
+		if (!$hadTransaction) {
+			$this->bd->commit();
+		}
+		return $result;
+	}
+
 	private $updateEntryPrepared = null;
 	private $updateEntryPrepared = null;
 
 
 	public function updateEntry($valuesTmp) {
 	public function updateEntry($valuesTmp) {
@@ -212,7 +264,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 
 
 		if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
 		if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
-			return $this->bd->lastInsertId();
+			return true;
 		} else {
 		} else {
 			$info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
 			$info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 			if ($this->autoUpdateDb($info)) {
@@ -578,18 +630,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$search .= 'AND ' . $alias . 'id >= ' . $date_min . '000000 ';
 			$search .= 'AND ' . $alias . 'id >= ' . $date_min . '000000 ';
 		}
 		}
 		if ($filter) {
 		if ($filter) {
-			if ($filter->getIntitle()) {
-				$search .= 'AND ' . $alias . 'title LIKE ? ';
-				$values[] = "%{$filter->getIntitle()}%";
-			}
-			if ($filter->getInurl()) {
-				$search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
-				$values[] = "%{$filter->getInurl()}%";
-			}
-			if ($filter->getAuthor()) {
-				$search .= 'AND ' . $alias . 'author LIKE ? ';
-				$values[] = "%{$filter->getAuthor()}%";
-			}
 			if ($filter->getMinDate()) {
 			if ($filter->getMinDate()) {
 				$search .= 'AND ' . $alias . 'id >= ? ';
 				$search .= 'AND ' . $alias . 'id >= ? ';
 				$values[] = "{$filter->getMinDate()}000000";
 				$values[] = "{$filter->getMinDate()}000000";
@@ -606,20 +646,69 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				$search .= 'AND ' . $alias . 'date <= ? ';
 				$search .= 'AND ' . $alias . 'date <= ? ';
 				$values[] = $filter->getMaxPubdate();
 				$values[] = $filter->getMaxPubdate();
 			}
 			}
+
+			if ($filter->getAuthor()) {
+				foreach ($filter->getAuthor() as $author) {
+					$search .= 'AND ' . $alias . 'author LIKE ? ';
+					$values[] = "%{$author}%";
+				}
+			}
+			if ($filter->getIntitle()) {
+				foreach ($filter->getIntitle() as $title) {
+					$search .= 'AND ' . $alias . 'title LIKE ? ';
+					$values[] = "%{$title}%";
+				}
+			}
 			if ($filter->getTags()) {
 			if ($filter->getTags()) {
-				$tags = $filter->getTags();
-				foreach ($tags as $tag) {
+				foreach ($filter->getTags() as $tag) {
 					$search .= 'AND ' . $alias . 'tags LIKE ? ';
 					$search .= 'AND ' . $alias . 'tags LIKE ? ';
 					$values[] = "%{$tag}%";
 					$values[] = "%{$tag}%";
 				}
 				}
 			}
 			}
+			if ($filter->getInurl()) {
+				foreach ($filter->getInurl() as $url) {
+					$search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
+					$values[] = "%{$url}%";
+				}
+			}
+
+			if ($filter->getNotAuthor()) {
+				foreach ($filter->getNotAuthor() as $author) {
+					$search .= 'AND (NOT ' . $alias . 'author LIKE ?) ';
+					$values[] = "%{$author}%";
+				}
+			}
+			if ($filter->getNotIntitle()) {
+				foreach ($filter->getNotIntitle() as $title) {
+					$search .= 'AND (NOT ' . $alias . 'title LIKE ?) ';
+					$values[] = "%{$title}%";
+				}
+			}
+			if ($filter->getNotTags()) {
+				foreach ($filter->getNotTags() as $tag) {
+					$search .= 'AND (NOT ' . $alias . 'tags LIKE ?) ';
+					$values[] = "%{$tag}%";
+				}
+			}
+			if ($filter->getNotInurl()) {
+				foreach ($filter->getNotInurl() as $url) {
+					$search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) ';
+					$values[] = "%{$url}%";
+				}
+			}
+
 			if ($filter->getSearch()) {
 			if ($filter->getSearch()) {
-				$search_values = $filter->getSearch();
-				foreach ($search_values as $search_value) {
+				foreach ($filter->getSearch() as $search_value) {
 					$search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
 					$search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
 					$values[] = "%{$search_value}%";
 					$values[] = "%{$search_value}%";
 				}
 				}
 			}
 			}
+			if ($filter->getNotSearch()) {
+				foreach ($filter->getNotSearch() as $search_value) {
+					$search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) ';
+					$values[] = "%{$search_value}%";
+				}
+			}
 		}
 		}
 		return array($values, $search);
 		return array($values, $search);
 	}
 	}

+ 26 - 0
app/Models/EntryDAOPGSQL.php

@@ -11,6 +11,11 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 	}
 	}
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
+		if (isset($errorInfo[0])) { 
+			if ($errorInfo[0] === '42P01' && stripos($errorInfo[2], 'entrytmp') !== false) {	//undefined_table
+				return $this->createEntryTempTable();
+			}
+		}
 		return false;
 		return false;
 	}
 	}
 
 
@@ -18,6 +23,27 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 		return false;
 		return false;
 	}
 	}
 
 
+	public function commitNewEntries() {
+		$sql = 'DO $$
+DECLARE
+maxrank bigint := (SELECT MAX(id) FROM `' . $this->prefix . 'entrytmp`);
+rank bigint := (SELECT maxrank - COUNT(*) FROM `' . $this->prefix . 'entrytmp`);
+BEGIN
+	INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
+		(SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date);
+	DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= maxrank;
+END $$;';
+		$hadTransaction = $this->bd->inTransaction();
+		if (!$hadTransaction) {
+			$this->bd->beginTransaction();
+		}
+		$result = $this->bd->exec($sql) !== false;
+		if (!$hadTransaction) {
+			$this->bd->commit();
+		}
+		return $result;
+	}
+
 	public function size($all = true) {
 	public function size($all = true) {
 		$db = FreshRSS_Context::$system_conf->db;
 		$db = FreshRSS_Context::$system_conf->db;
 		$sql = 'SELECT pg_size_pretty(pg_database_size(?))';
 		$sql = 'SELECT pg_size_pretty(pg_database_size(?))';

+ 30 - 9
app/Models/EntryDAOSQLite.php

@@ -7,21 +7,42 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 	}
 	}
 
 
 	protected function autoUpdateDb($errorInfo) {
 	protected function autoUpdateDb($errorInfo) {
-		if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') {	//ER_BAD_FIELD_ERROR
-			//autoAddColumn
-			if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
-				$showCreate = $tableInfo->fetchColumn();
-				Minz_Log::debug('FreshRSS_EntryDAOSQLite::autoUpdateDb: ' . $showCreate);
-				foreach (array('lastSeen', 'hash') as $column) {
-					if (stripos($showCreate, $column) === false) {
-						return $this->addColumn($column);
-					}
+		Minz_Log::error('FreshRSS_EntryDAO::autoUpdateDb error: ' . print_r($errorInfo, true));
+		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
+			$showCreate = $tableInfo->fetchColumn();
+			if (stripos($showCreate, 'entrytmp') === false) {
+				return $this->createEntryTempTable();
+			}
+		}
+		if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
+			$showCreate = $tableInfo->fetchColumn();
+			foreach (array('lastSeen', 'hash') as $column) {
+				if (stripos($showCreate, $column) === false) {
+					return $this->addColumn($column);
 				}
 				}
 			}
 			}
 		}
 		}
 		return false;
 		return false;
 	}
 	}
 
 
+	public function commitNewEntries() {
+		$sql = '
+CREATE TEMP TABLE `tmp` AS SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date;
+INSERT OR IGNORE INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
+	SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `tmp` ORDER BY date;
+DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`);
+DROP TABLE `tmp`;';
+		$hadTransaction = $this->bd->inTransaction();
+		if (!$hadTransaction) {
+			$this->bd->beginTransaction();
+		}
+		$result = $this->bd->exec($sql) !== false;
+		if (!$hadTransaction) {
+			$this->bd->commit();
+		}
+		return $result;
+	}
+
 	protected function sqlConcat($s1, $s2) {
 	protected function sqlConcat($s1, $s2) {
 		return $s1 . '||' . $s2;
 		return $s1 . '||' . $s2;
 	}
 	}

+ 1 - 8
app/Models/Factory.php

@@ -3,14 +3,7 @@
 class FreshRSS_Factory {
 class FreshRSS_Factory {
 
 
 	public static function createFeedDao($username = null) {
 	public static function createFeedDao($username = null) {
-		$conf = Minz_Configuration::get('system');
-		switch ($conf->db['type']) {
-			case 'sqlite':
-			case 'pgsql':
-				return new FreshRSS_FeedDAOSQLite($username);
-			default:
-				return new FreshRSS_FeedDAO($username);
-		}
+		return new FreshRSS_FeedDAO($username);
 	}
 	}
 
 
 	public static function createEntryDao($username = null) {
 	public static function createEntryDao($username = null) {

+ 8 - 7
app/Models/Feed.php

@@ -429,7 +429,7 @@ class FreshRSS_Feed extends Minz_Model {
 				}
 				}
 			} else {
 			} else {
 				@mkdir($path, 0777, true);
 				@mkdir($path, 0777, true);
-				$key = sha1($path . FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true));
+				$key = sha1($path . FreshRSS_Context::$system_conf->salt);
 				$hubJson = array(
 				$hubJson = array(
 					'hub' => $this->hubUrl,
 					'hub' => $this->hubUrl,
 					'key' => $key,
 					'key' => $key,
@@ -451,15 +451,16 @@ class FreshRSS_Feed extends Minz_Model {
 
 
 	//Parameter true to subscribe, false to unsubscribe.
 	//Parameter true to subscribe, false to unsubscribe.
 	function pubSubHubbubSubscribe($state) {
 	function pubSubHubbubSubscribe($state) {
-		if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) {
-			$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json';
+		$url = $this->selfUrl ? $this->selfUrl : $this->url;
+		if (FreshRSS_Context::$system_conf->base_url && $url) {
+			$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
 			$hubFile = @file_get_contents($hubFilename);
 			$hubFile = @file_get_contents($hubFilename);
 			if ($hubFile === false) {
 			if ($hubFile === false) {
 				Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url);
 				Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url);
 				return false;
 				return false;
 			}
 			}
 			$hubJson = json_decode($hubFile, true);
 			$hubJson = json_decode($hubFile, true);
-			if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
+			if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) {
 				Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url);
 				Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url);
 				return false;
 				return false;
 			}
 			}
@@ -474,13 +475,13 @@ class FreshRSS_Feed extends Minz_Model {
 			}
 			}
 			$ch = curl_init();
 			$ch = curl_init();
 			curl_setopt_array($ch, array(
 			curl_setopt_array($ch, array(
-				CURLOPT_URL => $this->hubUrl,
+				CURLOPT_URL => $hubJson['hub'],
 				CURLOPT_FOLLOWLOCATION => true,
 				CURLOPT_FOLLOWLOCATION => true,
 				CURLOPT_RETURNTRANSFER => true,
 				CURLOPT_RETURNTRANSFER => true,
 				CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
 				CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
 				CURLOPT_POSTFIELDS => 'hub.verify=sync'
 				CURLOPT_POSTFIELDS => 'hub.verify=sync'
 					. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
 					. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
-					. '&hub.topic=' . urlencode($this->selfUrl)
+					. '&hub.topic=' . urlencode($url)
 					. '&hub.callback=' . urlencode($callbackUrl)
 					. '&hub.callback=' . urlencode($callbackUrl)
 				)
 				)
 			);
 			);
@@ -488,7 +489,7 @@ class FreshRSS_Feed extends Minz_Model {
 			$info = curl_getinfo($ch);
 			$info = curl_getinfo($ch);
 
 
 			file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" .
 			file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" .
-				'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl .
+				'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $url .
 				' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND);
 				' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND);
 
 
 			if (substr($info['http_code'], 0, 1) == '2') {
 			if (substr($info['http_code'], 0, 1) == '2') {

+ 26 - 30
app/Models/FeedDAO.php

@@ -92,29 +92,15 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		}
 	}
 	}
 
 
-	public function updateLastUpdate($id, $inError = false, $updateCache = true, $mtime = 0) {
-		if ($updateCache) {
-			$sql = 'UPDATE `' . $this->prefix . 'feed` '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
-			     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
-			     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
-			     . '`lastUpdate`=?, error=? '
-			     . 'WHERE id=?';
-		} else {
-			$sql = 'UPDATE `' . $this->prefix . 'feed` '
-			     . 'SET `lastUpdate`=?, error=? '
-			     . 'WHERE id=?';
-		}
-
-		if ($mtime <= 0) {
-			$mtime = time();
-		}
-
+	public function updateLastUpdate($id, $inError = false, $mtime = 0) {	//See also updateCachedValue()
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		     . 'SET `lastUpdate`=?, error=? '
+		     . 'WHERE id=?';
 		$values = array(
 		$values = array(
-			$mtime,
+			$mtime <= 0 ? time() : $mtime,
 			$inError ? 1 : 0,
 			$inError ? 1 : 0,
 			$id,
 			$id,
 		);
 		);
-
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 
 		if ($stm && $stm->execute($values)) {
 		if ($stm && $stm->execute($values)) {
@@ -294,18 +280,28 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $res[0]['count'];
 		return $res[0]['count'];
 	}
 	}
 
 
-	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
-		$sql = 'UPDATE `' . $this->prefix . 'feed` f '
-		     . 'INNER JOIN ('
-		     .	'SELECT e.id_feed, '
-		     .	'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, '
-		     .	'COUNT(e.id) AS nbEntries '
-		     .	'FROM `' . $this->prefix . 'entry` e '
-		     .	'GROUP BY e.id_feed'
-		     . ') x ON x.id_feed=f.id '
-		     . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads';
+	public function updateCachedValue($id) {	//For multiple feeds, call updateCachedValues()
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
+		     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+		     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0) '
+		     . 'WHERE id=?';
+		$values = array($id);
 		$stm = $this->bd->prepare($sql);
 		$stm = $this->bd->prepare($sql);
 
 
+		if ($stm && $stm->execute($values)) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error updateCachedValue: ' . $info[2]);
+			return false;
+		}
+	}
+
+	public function updateCachedValues() {	//For one single feed, call updateCachedValue($id)
+		$sql = 'UPDATE `' . $this->prefix . 'feed` '
+		     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+		     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
+		$stm = $this->bd->prepare($sql);
 		if ($stm && $stm->execute()) {
 		if ($stm && $stm->execute()) {
 			return $stm->rowCount();
 			return $stm->rowCount();
 		} else {
 		} else {
@@ -343,7 +339,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $affected;
 		return $affected;
 	}
 	}
 
 
-	public function cleanOldEntries($id, $date_min, $keep = 15) {	//Remember to call updateLastUpdate($id) or updateCachedValues() just after
+	public function cleanOldEntries($id, $date_min, $keep = 15) {	//Remember to call updateCachedValue($id) or updateCachedValues() just after
 		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
 		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
 		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
 		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
 		     . 'AND is_favorite=0 '	//Do not remove favourites
 		     . 'AND is_favorite=0 '	//Do not remove favourites

+ 0 - 19
app/Models/FeedDAOSQLite.php

@@ -1,19 +0,0 @@
-<?php
-
-class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
-
-	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
-		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-		     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
-		     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
-		$stm = $this->bd->prepare($sql);
-		if ($stm && $stm->execute()) {
-			return $stm->rowCount();
-		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error updateCachedValues: ' . $info[2]);
-			return false;
-		}
-	}
-
-}

+ 139 - 29
app/Models/Search.php

@@ -23,18 +23,35 @@ class FreshRSS_Search {
 	private $tags;
 	private $tags;
 	private $search;
 	private $search;
 
 
+	private $not_intitle;
+	private $not_inurl;
+	private $not_author;
+	private $not_tags;
+	private $not_search;
+
 	public function __construct($input) {
 	public function __construct($input) {
-		if (strcmp($input, '') == 0) {
+		if ($input == '') {
 			return;
 			return;
 		}
 		}
 		$this->raw_input = $input;
 		$this->raw_input = $input;
+
+		$input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
+
+		$input = $this->parseNotIntitleSearch($input);
+		$input = $this->parseNotAuthorSearch($input);
+		$input = $this->parseNotInurlSearch($input);
+		$input = $this->parseNotTagsSeach($input);
+
+		$input = $this->parsePubdateSearch($input);
+		$input = $this->parseDateSearch($input);
+
 		$input = $this->parseIntitleSearch($input);
 		$input = $this->parseIntitleSearch($input);
 		$input = $this->parseAuthorSearch($input);
 		$input = $this->parseAuthorSearch($input);
 		$input = $this->parseInurlSearch($input);
 		$input = $this->parseInurlSearch($input);
-		$input = $this->parsePubdateSearch($input);
-		$input = $this->parseDateSearch($input);
 		$input = $this->parseTagsSeach($input);
 		$input = $this->parseTagsSeach($input);
-		$this->parseSearch($input);
+
+		$input = $this->parseNotSearch($input);
+		$input = $this->parseSearch($input);
 	}
 	}
 
 
 	public function __toString() {
 	public function __toString() {
@@ -48,6 +65,9 @@ class FreshRSS_Search {
 	public function getIntitle() {
 	public function getIntitle() {
 		return $this->intitle;
 		return $this->intitle;
 	}
 	}
+	public function getNotIntitle() {
+		return $this->not_intitle;
+	}
 
 
 	public function getMinDate() {
 	public function getMinDate() {
 		return $this->min_date;
 		return $this->min_date;
@@ -68,18 +88,34 @@ class FreshRSS_Search {
 	public function getInurl() {
 	public function getInurl() {
 		return $this->inurl;
 		return $this->inurl;
 	}
 	}
+	public function getNotInurl() {
+		return $this->not_inurl;
+	}
 
 
 	public function getAuthor() {
 	public function getAuthor() {
 		return $this->author;
 		return $this->author;
 	}
 	}
+	public function getNotAuthor() {
+		return $this->not_author;
+	}
 
 
 	public function getTags() {
 	public function getTags() {
 		return $this->tags;
 		return $this->tags;
 	}
 	}
+	public function getNotTags() {
+		return $this->not_tags;
+	}
 
 
 	public function getSearch() {
 	public function getSearch() {
 		return $this->search;
 		return $this->search;
 	}
 	}
+	public function getNotSearch() {
+		return $this->not_search;
+	}
+
+	private static function removeEmptyValues($anArray) {
+		return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array();
+	}
 
 
 	/**
 	/**
 	 * Parse the search string to find intitle keyword and the search related
 	 * Parse the search string to find intitle keyword and the search related
@@ -90,14 +126,28 @@ class FreshRSS_Search {
 	 * @return string
 	 * @return string
 	 */
 	 */
 	private function parseIntitleSearch($input) {
 	private function parseIntitleSearch($input) {
-		if (preg_match('/intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+		if (preg_match_all('/\bintitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 			$this->intitle = $matches['search'];
 			$this->intitle = $matches['search'];
-			return str_replace($matches[0], '', $input);
+			$input = str_replace($matches[0], '', $input);
 		}
 		}
-		if (preg_match('/intitle:(?P<search>\w*)/', $input, $matches)) {
-			$this->intitle = $matches['search'];
-			return str_replace($matches[0], '', $input);
+		if (preg_match_all('/\bintitle:(?P<search>\w*)/', $input, $matches)) {
+			$this->intitle = array_merge($this->intitle ? $this->intitle : array(), $matches['search']);
+			$input = str_replace($matches[0], '', $input);
+		}
+		$this->intitle = self::removeEmptyValues($this->intitle);
+		return $input;
+	}
+
+	private function parseNotIntitleSearch($input) {
+		if (preg_match_all('/[!-]intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+			$this->not_intitle = $matches['search'];
+			$input = str_replace($matches[0], '', $input);
+		}
+		if (preg_match_all('/[!-]intitle:(?P<search>\w*)/', $input, $matches)) {
+			$this->not_intitle = array_merge($this->not_intitle ? $this->not_intitle : array(), $matches['search']);
+			$input = str_replace($matches[0], '', $input);
 		}
 		}
+		$this->not_intitle = self::removeEmptyValues($this->not_intitle);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -112,30 +162,54 @@ class FreshRSS_Search {
 	 * @return string
 	 * @return string
 	 */
 	 */
 	private function parseAuthorSearch($input) {
 	private function parseAuthorSearch($input) {
-		if (preg_match('/author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+		if (preg_match_all('/\bauthor:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 			$this->author = $matches['search'];
 			$this->author = $matches['search'];
-			return str_replace($matches[0], '', $input);
+			$input = str_replace($matches[0], '', $input);
 		}
 		}
-		if (preg_match('/author:(?P<search>\w*)/', $input, $matches)) {
-			$this->author = $matches['search'];
-			return str_replace($matches[0], '', $input);
+		if (preg_match_all('/\bauthor:(?P<search>\w*)/', $input, $matches)) {
+			$this->author = array_merge($this->author ? $this->author : array(), $matches['search']);
+			$input = str_replace($matches[0], '', $input);
 		}
 		}
+		$this->author = self::removeEmptyValues($this->author);
+		return $input;
+	}
+
+	private function parseNotAuthorSearch($input) {
+		if (preg_match_all('/[!-]author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+			$this->not_author = $matches['search'];
+			$input = str_replace($matches[0], '', $input);
+		}
+		if (preg_match_all('/[!-]author:(?P<search>\w*)/', $input, $matches)) {
+			$this->not_author = array_merge($this->not_author ? $this->not_author : array(), $matches['search']);
+			$input = str_replace($matches[0], '', $input);
+		}
+		$this->not_author = self::removeEmptyValues($this->not_author);
 		return $input;
 		return $input;
 	}
 	}
 
 
 	/**
 	/**
 	 * Parse the search string to find inurl keyword and the search related
 	 * Parse the search string to find inurl keyword and the search related
 	 * to it.
 	 * to it.
-	 * The search is the first word following the keyword except.
+	 * The search is the first word following the keyword.
 	 *
 	 *
 	 * @param string $input
 	 * @param string $input
 	 * @return string
 	 * @return string
 	 */
 	 */
 	private function parseInurlSearch($input) {
 	private function parseInurlSearch($input) {
-		if (preg_match('/inurl:(?P<search>[^\s]*)/', $input, $matches)) {
+		if (preg_match_all('/\binurl:(?P<search>[^\s]*)/', $input, $matches)) {
 			$this->inurl = $matches['search'];
 			$this->inurl = $matches['search'];
-			return str_replace($matches[0], '', $input);
+			$input = str_replace($matches[0], '', $input);
+		}
+		$this->inurl = self::removeEmptyValues($this->inurl);
+		return $input;
+	}
+
+	private function parseNotInurlSearch($input) {
+		if (preg_match_all('/[!-]inurl:(?P<search>[^\s]*)/', $input, $matches)) {
+			$this->not_inurl = $matches['search'];
+			$input = str_replace($matches[0], '', $input);
 		}
 		}
+		$this->not_inurl = self::removeEmptyValues($this->not_inurl);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -148,9 +222,12 @@ class FreshRSS_Search {
 	 * @return string
 	 * @return string
 	 */
 	 */
 	private function parseDateSearch($input) {
 	private function parseDateSearch($input) {
-		if (preg_match('/date:(?P<search>[^\s]*)/', $input, $matches)) {
-			list($this->min_date, $this->max_date) = parseDateInterval($matches['search']);
-			return str_replace($matches[0], '', $input);
+		if (preg_match_all('/\bdate:(?P<search>[^\s]*)/', $input, $matches)) {
+			$input = str_replace($matches[0], '', $input);
+			$dates = self::removeEmptyValues($matches['search']);
+			if (!empty($dates[0])) {
+				list($this->min_date, $this->max_date) = parseDateInterval($dates[0]);
+			}
 		}
 		}
 		return $input;
 		return $input;
 	}
 	}
@@ -164,9 +241,12 @@ class FreshRSS_Search {
 	 * @return string
 	 * @return string
 	 */
 	 */
 	private function parsePubdateSearch($input) {
 	private function parsePubdateSearch($input) {
-		if (preg_match('/pubdate:(?P<search>[^\s]*)/', $input, $matches)) {
-			list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($matches['search']);
-			return str_replace($matches[0], '', $input);
+		if (preg_match_all('/\bpubdate:(?P<search>[^\s]*)/', $input, $matches)) {
+			$input = str_replace($matches[0], '', $input);
+			$dates = self::removeEmptyValues($matches['search']);
+			if (!empty($dates[0])) {
+				list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($dates[0]);
+			}
 		}
 		}
 		return $input;
 		return $input;
 	}
 	}
@@ -182,8 +262,18 @@ class FreshRSS_Search {
 	private function parseTagsSeach($input) {
 	private function parseTagsSeach($input) {
 		if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
 		if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
 			$this->tags = $matches['search'];
 			$this->tags = $matches['search'];
-			return str_replace($matches[0], '', $input);
+			$input = str_replace($matches[0], '', $input);
 		}
 		}
+		$this->tags = self::removeEmptyValues($this->tags);
+		return $input;
+	}
+
+	private function parseNotTagsSeach($input) {
+		if (preg_match_all('/[!-]#(?P<search>[^\s]+)/', $input, $matches)) {
+			$this->not_tags = $matches['search'];
+			$input = str_replace($matches[0], '', $input);
+		}
+		$this->not_tags = self::removeEmptyValues($this->not_tags);
 		return $input;
 		return $input;
 	}
 	}
 
 
@@ -196,16 +286,16 @@ class FreshRSS_Search {
 	 * @return string
 	 * @return string
 	 */
 	 */
 	private function parseSearch($input) {
 	private function parseSearch($input) {
-		$input = $this->cleanSearch($input);
-		if (strcmp($input, '') == 0) {
+		$input = self::cleanSearch($input);
+		if ($input == '') {
 			return;
 			return;
 		}
 		}
 		if (preg_match_all('/(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 		if (preg_match_all('/(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 			$this->search = $matches['search'];
 			$this->search = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
 			$input = str_replace($matches[0], '', $input);
 		}
 		}
-		$input = $this->cleanSearch($input);
-		if (strcmp($input, '') == 0) {
+		$input = self::cleanSearch($input);
+		if ($input == '') {
 			return;
 			return;
 		}
 		}
 		if (is_array($this->search)) {
 		if (is_array($this->search)) {
@@ -215,13 +305,33 @@ class FreshRSS_Search {
 		}
 		}
 	}
 	}
 
 
+	private function parseNotSearch($input) {
+		$input = self::cleanSearch($input);
+		if ($input == '') {
+			return;
+		}
+		if (preg_match_all('/[!-](?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+			$this->not_search = $matches['search'];
+			$input = str_replace($matches[0], '', $input);
+		}
+		if ($input == '') {
+			return;
+		}
+		if (preg_match_all('/[!-](?P<search>[^\s]+)/', $input, $matches)) {
+			$this->not_search = array_merge(is_array($this->not_search) ? $this->not_search : array(), $matches['search']);
+			$input = str_replace($matches[0], '', $input);
+		}
+		$this->not_search = self::removeEmptyValues($this->not_search);
+		return $input;
+	}
+
 	/**
 	/**
 	 * Remove all unnecessary spaces in the search
 	 * Remove all unnecessary spaces in the search
 	 *
 	 *
 	 * @param string $input
 	 * @param string $input
 	 * @return string
 	 * @return string
 	 */
 	 */
-	private function cleanSearch($input) {
+	private static function cleanSearch($input) {
 		$input = preg_replace('/\s+/', ' ', $input);
 		$input = preg_replace('/\s+/', ' ', $input);
 		return trim($input);
 		return trim($input);
 	}
 	}

+ 6 - 4
app/Models/UserDAO.php

@@ -14,21 +14,23 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 			$ok = false;
 			$ok = false;
 			$bd_prefix_user = $db['prefix'] . $username . '_';
 			$bd_prefix_user = $db['prefix'] . $username . '_';
 			if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
 			if (defined('SQL_CREATE_TABLES')) {	//E.g. MySQL
-				$sql = sprintf(SQL_CREATE_TABLES, $bd_prefix_user, _t('gen.short.default_category'));
+				$sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP, $bd_prefix_user, _t('gen.short.default_category'));
 				$stm = $userPDO->bd->prepare($sql);
 				$stm = $userPDO->bd->prepare($sql);
 				$ok = $stm && $stm->execute();
 				$ok = $stm && $stm->execute();
 			} else {	//E.g. SQLite
 			} else {	//E.g. SQLite
 				global $SQL_CREATE_TABLES;
 				global $SQL_CREATE_TABLES;
+				global $SQL_CREATE_TABLE_ENTRYTMP;
 				if (is_array($SQL_CREATE_TABLES)) {
 				if (is_array($SQL_CREATE_TABLES)) {
-					$ok = true;
-					foreach ($SQL_CREATE_TABLES as $instruction) {
+					$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP);
+					$ok = !empty($instructions);
+					foreach ($instructions as $instruction) {
 						$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
 						$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
 						$stm = $userPDO->bd->prepare($sql);
 						$stm = $userPDO->bd->prepare($sql);
 						$ok &= ($stm && $stm->execute());
 						$ok &= ($stm && $stm->execute());
 					}
 					}
 				}
 				}
 			}
 			}
-			if ($insertDefaultFeeds) {
+			if ($ok && $insertDefaultFeeds) {
 				if (defined('SQL_INSERT_FEEDS')) {	//E.g. MySQL
 				if (defined('SQL_INSERT_FEEDS')) {	//E.g. MySQL
 					$sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
 					$sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
 					$stm = $userPDO->bd->prepare($sql);
 					$stm = $userPDO->bd->prepare($sql);

+ 27 - 1
app/SQL/install.sql.mysql.php

@@ -55,18 +55,44 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` (
 	INDEX (`is_favorite`),	-- v0.7
 	INDEX (`is_favorite`),	-- v0.7
 	INDEX (`is_read`),	-- v0.7
 	INDEX (`is_read`),	-- v0.7
 	INDEX `entry_lastSeen_index` (`lastSeen`)	-- v1.1.1
 	INDEX `entry_lastSeen_index` (`lastSeen`)	-- v1.1.1
+	-- INDEX `entry_feed_read_index` (`id_feed`,`is_read`)	-- v1.7 Located futher down
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 ENGINE = INNODB;
 
 
 INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
 INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
 ');
 ');
 
 
+define('SQL_CREATE_TABLE_ENTRYTMP', '
+CREATE TABLE IF NOT EXISTS `%1$sentrytmp` (	-- v1.7
+	`id` bigint NOT NULL,
+	`guid` varchar(760) CHARACTER SET latin1 NOT NULL,
+	`title` varchar(255) NOT NULL,
+	`author` varchar(255),
+	`content_bin` blob,
+	`link` varchar(1023) CHARACTER SET latin1 NOT NULL,
+	`date` int(11),
+	`lastSeen` INT(11) DEFAULT 0,
+	`hash` BINARY(16),
+	`is_read` boolean NOT NULL DEFAULT 0,
+	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`id_feed` SMALLINT,
+	`tags` varchar(1023),
+	PRIMARY KEY (`id`),
+	FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE KEY (`id_feed`,`guid`),
+	INDEX (`date`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
+ENGINE = INNODB;
+
+CREATE INDEX `entry_feed_read_index` ON `%1$sentry`(`id_feed`,`is_read`);	-- v1.7 Located here to be auto-added
+');
+
 define('SQL_INSERT_FEEDS', '
 define('SQL_INSERT_FEEDS', '
 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("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);
 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 TABLE IF EXISTS `%1$sentry`, `%1$sfeed`, `%1$scategory`');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentrytmp`, `%1$sentry`, `%1$sfeed`, `%1$scategory`');
 
 
 define('SQL_UPDATE_UTF8MB4', '
 define('SQL_UPDATE_UTF8MB4', '
 ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

+ 26 - 2
app/SQL/install.sql.pgsql.php

@@ -32,7 +32,7 @@ $SQL_CREATE_TABLES = array(
 
 
 'CREATE TABLE IF NOT EXISTS "%1$sentry" (
 'CREATE TABLE IF NOT EXISTS "%1$sentry" (
 	"id" BIGINT NOT NULL PRIMARY KEY,
 	"id" BIGINT NOT NULL PRIMARY KEY,
-	"guid" VARCHAR(760) UNIQUE NOT NULL,
+	"guid" VARCHAR(760) NOT NULL,
 	"title" VARCHAR(255) NOT NULL,
 	"title" VARCHAR(255) NOT NULL,
 	"author" VARCHAR(255),
 	"author" VARCHAR(255),
 	"content" TEXT,
 	"content" TEXT,
@@ -54,10 +54,34 @@ $SQL_CREATE_TABLES = array(
 'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);',
 'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);',
 );
 );
 
 
+global $SQL_CREATE_TABLE_ENTRYTMP;
+$SQL_CREATE_TABLE_ENTRYTMP = array(
+'CREATE TABLE IF NOT EXISTS "%1$sentrytmp" (	-- v1.7
+	"id" BIGINT NOT NULL PRIMARY KEY,
+	"guid" VARCHAR(760) NOT NULL,
+	"title" VARCHAR(255) NOT NULL,
+	"author" VARCHAR(255),
+	"content" TEXT,
+	"link" VARCHAR(1023) NOT NULL,
+	"date" INT,
+	"lastSeen" INT DEFAULT 0,
+	"hash" BYTEA,
+	"is_read" SMALLINT NOT NULL DEFAULT 0,
+	"is_favorite" SMALLINT NOT NULL DEFAULT 0,
+	"id_feed" SMALLINT,
+	"tags" VARCHAR(1023),
+	FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE ("id_feed","guid")
+);',
+'CREATE INDEX %1$sentrytmp_date_index ON "%1$sentrytmp" ("date");',
+
+'CREATE INDEX %1$sentry_feed_read_index ON "%1$sentry" ("id_feed","is_read");',	//v1.7
+);
+
 global $SQL_INSERT_FEEDS;
 global $SQL_INSERT_FEEDS;
 $SQL_INSERT_FEEDS = array(
 $SQL_INSERT_FEEDS = array(
 'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');',
 'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');',
 'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');',
 'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');',
 );
 );
 
 
-define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentry", "%1$sfeed", "%1$scategory"');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentrytmp", "%1$sentry", "%1$sfeed", "%1$scategory"');

+ 26 - 3
app/SQL/install.sql.sqlite.php

@@ -26,7 +26,6 @@ $SQL_CREATE_TABLES = array(
 	FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	UNIQUE (`url`)
 	UNIQUE (`url`)
 );',
 );',
-
 'CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);',
 'CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);',
 'CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);',
 'CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);',
 'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
 'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
@@ -49,7 +48,6 @@ $SQL_CREATE_TABLES = array(
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 	UNIQUE (`id_feed`,`guid`)
 	UNIQUE (`id_feed`,`guid`)
 );',
 );',
-
 'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `entry`(`is_favorite`);',
 'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `entry`(`is_favorite`);',
 'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `entry`(`is_read`);',
 'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `entry`(`is_read`);',
 'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `entry`(`lastSeen`);',	//v1.1.1
 'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `entry`(`lastSeen`);',	//v1.1.1
@@ -57,10 +55,35 @@ $SQL_CREATE_TABLES = array(
 'INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "%2$s");',
 'INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "%2$s");',
 );
 );
 
 
+global $SQL_CREATE_TABLE_ENTRYTMP;
+$SQL_CREATE_TABLE_ENTRYTMP = array(
+'CREATE TABLE IF NOT EXISTS `entrytmp` (	-- v1.7
+	`id` bigint NOT NULL,
+	`guid` varchar(760) NOT NULL,
+	`title` varchar(255) NOT NULL,
+	`author` varchar(255),
+	`content` text,
+	`link` varchar(1023) NOT NULL,
+	`date` int(11),
+	`lastSeen` INT(11) DEFAULT 0,
+	`hash` BINARY(16),
+	`is_read` boolean NOT NULL DEFAULT 0,
+	`is_favorite` boolean NOT NULL DEFAULT 0,
+	`id_feed` SMALLINT,
+	`tags` varchar(1023),
+	PRIMARY KEY (`id`),
+	FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE (`id_feed`,`guid`)
+);',
+'CREATE INDEX IF NOT EXISTS entrytmp_date_index ON `entrytmp`(`date`);',
+
+'CREATE INDEX IF NOT EXISTS `entry_feed_read_index` ON `entry`(`id_feed`,`is_read`);',	//v1.7
+);
+
 global $SQL_INSERT_FEEDS;
 global $SQL_INSERT_FEEDS;
 $SQL_INSERT_FEEDS = array(
 $SQL_INSERT_FEEDS = array(
 'INSERT OR IGNORE INTO `feed` (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 `feed` (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 `feed` (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);',
 'INSERT OR IGNORE INTO `feed` (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 TABLE IF EXISTS entry, feed, category');
+define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `entrytmp`, `entry`, `feed`, `category`');

+ 10 - 10
app/i18n/en/admin.php

@@ -21,11 +21,11 @@ return array(
 			'ok' => 'Permissions on cache directory are good.',
 			'ok' => 'Permissions on cache directory are good.',
 		),
 		),
 		'categories' => array(
 		'categories' => array(
-			'nok' => 'Category table is bad configured.',
+			'nok' => 'Category table is improperly configured.',
 			'ok' => 'Category table is ok.',
 			'ok' => 'Category table is ok.',
 		),
 		),
 		'connection' => array(
 		'connection' => array(
-			'nok' => 'Connection to the database cannot being established.',
+			'nok' => 'Connection to the database cannot be established.',
 			'ok' => 'Connection to the database is ok.',
 			'ok' => 'Connection to the database is ok.',
 		),
 		),
 		'ctype' => array(
 		'ctype' => array(
@@ -46,7 +46,7 @@ return array(
 			'ok' => 'You have the required library to browse the DOM.',
 			'ok' => 'You have the required library to browse the DOM.',
 		),
 		),
 		'entries' => array(
 		'entries' => array(
-			'nok' => 'Entry table is bad configured.',
+			'nok' => 'Entry table is improperly configured.',
 			'ok' => 'Entry table is ok.',
 			'ok' => 'Entry table is ok.',
 		),
 		),
 		'favicons' => array(
 		'favicons' => array(
@@ -54,7 +54,7 @@ return array(
 			'ok' => 'Permissions on favicons directory are good.',
 			'ok' => 'Permissions on favicons directory are good.',
 		),
 		),
 		'feeds' => array(
 		'feeds' => array(
-			'nok' => 'Feed table is bad configured.',
+			'nok' => 'Feed table is improperly configured.',
 			'ok' => 'Feed table is ok.',
 			'ok' => 'Feed table is ok.',
 		),
 		),
 		'fileinfo' => array(
 		'fileinfo' => array(
@@ -84,8 +84,8 @@ return array(
 			'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.',
 			'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.',
 		),
 		),
 		'tables' => array(
 		'tables' => array(
-			'nok' => 'There is one or more lacking tables in the database.',
-			'ok' => 'Tables are existing in the database.',
+			'nok' => 'There are one or more missing tables in the database.',
+			'ok' => 'The appropriate tables exist in the database.',
 		),
 		),
 		'title' => 'Installation checking',
 		'title' => 'Installation checking',
 		'tokens' => array(
 		'tokens' => array(
@@ -103,7 +103,7 @@ return array(
 	),
 	),
 	'extensions' => array(
 	'extensions' => array(
 		'disabled' => 'Disabled',
 		'disabled' => 'Disabled',
-		'empty_list' => 'There is no installed extension',
+		'empty_list' => 'There are no installed extensions',
 		'enabled' => 'Enabled',
 		'enabled' => 'Enabled',
 		'no_configure_view' => 'This extension cannot be configured.',
 		'no_configure_view' => 'This extension cannot be configured.',
 		'system' => array(
 		'system' => array(
@@ -160,7 +160,7 @@ return array(
 		'_' => 'Update system',
 		'_' => 'Update system',
 		'apply' => 'Apply',
 		'apply' => 'Apply',
 		'check' => 'Check for new updates',
 		'check' => 'Check for new updates',
-		'current_version' => 'Your current version of FreshRSS is the %s.',
+		'current_version' => 'Your current version of FreshRSS is %s.',
 		'last' => 'Last verification: %s',
 		'last' => 'Last verification: %s',
 		'none' => 'No update to apply',
 		'none' => 'No update to apply',
 		'title' => 'Update system',
 		'title' => 'Update system',
@@ -169,8 +169,8 @@ return array(
 		'articles_and_size' => '%s articles (%s)',
 		'articles_and_size' => '%s articles (%s)',
 		'create' => 'Create new user',
 		'create' => 'Create new user',
 		'language' => 'Language',
 		'language' => 'Language',
-		'number' => 'There is %d account created yet',
-		'numbers' => 'There are %d accounts created yet',
+		'number' => 'There is %d account created',
+		'numbers' => 'There are %d accounts created',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_format' => 'At least 7 characters',
 		'password_format' => 'At least 7 characters',
 		'title' => 'Manage users',
 		'title' => 'Manage users',

+ 18 - 18
app/i18n/en/conf.php

@@ -8,7 +8,7 @@ return array(
 		'help' => 'More options are available in the individual feed settings',
 		'help' => 'More options are available in the individual feed settings',
 		'keep_history_by_feed' => 'Minimum number of articles to keep by feed',
 		'keep_history_by_feed' => 'Minimum number of articles to keep by feed',
 		'optimize' => 'Optimise database',
 		'optimize' => 'Optimise database',
-		'optimize_help' => 'To do occasionally to reduce the size of the database',
+		'optimize_help' => 'Do occasionally to reduce the size of the database',
 		'purge_now' => 'Purge now',
 		'purge_now' => 'Purge now',
 		'title' => 'Archiving',
 		'title' => 'Archiving',
 		'ttl' => 'Do not automatically refresh more often than',
 		'ttl' => 'Do not automatically refresh more often than',
@@ -44,10 +44,10 @@ return array(
 		'filter' => 'Filter applied:',
 		'filter' => 'Filter applied:',
 		'get_all' => 'Display all articles',
 		'get_all' => 'Display all articles',
 		'get_category' => 'Display "%s" category',
 		'get_category' => 'Display "%s" category',
-		'get_favorite' => 'Display favorite articles',
+		'get_favorite' => 'Display favourite articles',
 		'get_feed' => 'Display "%s" feed',
 		'get_feed' => 'Display "%s" feed',
 		'no_filter' => 'No filter',
 		'no_filter' => 'No filter',
-		'none' => 'You haven’t created any user query yet.',
+		'none' => 'You haven’t created any user queries yet.',
 		'number' => 'Query n°%d',
 		'number' => 'Query n°%d',
 		'order_asc' => 'Display oldest articles first',
 		'order_asc' => 'Display oldest articles first',
 		'order_desc' => 'Display newest articles first',
 		'order_desc' => 'Display newest articles first',
@@ -56,14 +56,14 @@ return array(
 		'state_1' => 'Display read articles',
 		'state_1' => 'Display read articles',
 		'state_2' => 'Display unread articles',
 		'state_2' => 'Display unread articles',
 		'state_3' => 'Display all articles',
 		'state_3' => 'Display all articles',
-		'state_4' => 'Display favorite articles',
-		'state_5' => 'Display read favorite articles',
-		'state_6' => 'Display unread favorite articles',
-		'state_7' => 'Display favorite articles',
-		'state_8' => 'Display not favorite articles',
-		'state_9' => 'Display read not favorite articles',
-		'state_10' => 'Display unread not favorite articles',
-		'state_11' => 'Display not favorite articles',
+		'state_4' => 'Display favourite articles',
+		'state_5' => 'Display read favourite articles',
+		'state_6' => 'Display unread favourite articles',
+		'state_7' => 'Display favourite articles',
+		'state_8' => 'Display not favourite articles',
+		'state_9' => 'Display read not favourite articles',
+		'state_10' => 'Display unread not favourite articles',
+		'state_11' => 'Display not favourite articles',
 		'state_12' => 'Display all articles',
 		'state_12' => 'Display all articles',
 		'state_13' => 'Display read articles',
 		'state_13' => 'Display read articles',
 		'state_14' => 'Display unread articles',
 		'state_14' => 'Display unread articles',
@@ -74,7 +74,7 @@ return array(
 		'_' => 'Profile management',
 		'_' => 'Profile management',
 		'delete' => array(
 		'delete' => array(
 			'_' => 'Account deletion',
 			'_' => 'Account deletion',
-			'warn' => 'Your account and all the related data will be deleted.',
+			'warn' => 'Your account and all related data will be deleted.',
 		),
 		),
 		'password_api' => 'API password<br /><small>(e.g., for mobile apps)</small>',
 		'password_api' => 'API password<br /><small>(e.g., for mobile apps)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
@@ -85,13 +85,13 @@ return array(
 		'_' => 'Reading',
 		'_' => 'Reading',
 		'after_onread' => 'After “mark all as read”,',
 		'after_onread' => 'After “mark all as read”,',
 		'articles_per_page' => 'Number of articles per page',
 		'articles_per_page' => 'Number of articles per page',
-		'auto_load_more' => 'Load next articles at the page bottom',
+		'auto_load_more' => 'Load more articles at the page bottom',
 		'auto_remove_article' => 'Hide articles after reading',
 		'auto_remove_article' => 'Hide articles after reading',
 		'mark_updated_article_unread' => 'Mark updated articles as unread',
 		'mark_updated_article_unread' => 'Mark updated articles as unread',
 		'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions',
 		'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions',
 		'display_articles_unfolded' => 'Show articles unfolded by default',
 		'display_articles_unfolded' => 'Show articles unfolded by default',
 		'display_categories_unfolded' => 'Show categories folded by default',
 		'display_categories_unfolded' => 'Show categories folded by default',
-		'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)',
+		'hide_read_feeds' => 'Hide categories & feeds with no unread articles (does not work with “Show all articles” configuration)',
 		'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
 		'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
 		'jump_next' => 'jump to next unread sibling (feed or category)',
 		'jump_next' => 'jump to next unread sibling (feed or category)',
 		'number_divided_when_reader' => 'Divided by 2 in the reading view.',
 		'number_divided_when_reader' => 'Divided by 2 in the reading view.',
@@ -99,7 +99,7 @@ return array(
 			'article_open_on_website' => 'when article is opened on its original website',
 			'article_open_on_website' => 'when article is opened on its original website',
 			'article_viewed' => 'when article is viewed',
 			'article_viewed' => 'when article is viewed',
 			'scroll' => 'while scrolling',
 			'scroll' => 'while scrolling',
-			'upon_reception' => 'upon reception of the article',
+			'upon_reception' => 'upon receiving the article',
 			'when' => 'Mark article as read…',
 			'when' => 'Mark article as read…',
 		),
 		),
 		'show' => array(
 		'show' => array(
@@ -110,7 +110,7 @@ return array(
 		),
 		),
 		'sort' => array(
 		'sort' => array(
 			'_' => 'Sort order',
 			'_' => 'Sort order',
-			'newer_first' => 'Newer first',
+			'newer_first' => 'Newest first',
 			'older_first' => 'Oldest first',
 			'older_first' => 'Oldest first',
 		),
 		),
 		'sticky_post' => 'Stick the article to the top when opened',
 		'sticky_post' => 'Stick the article to the top when opened',
@@ -142,7 +142,7 @@ return array(
 		'_' => 'Shortcuts',
 		'_' => 'Shortcuts',
 		'article_action' => 'Article actions',
 		'article_action' => 'Article actions',
 		'auto_share' => 'Share',
 		'auto_share' => 'Share',
-		'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.',
+		'auto_share_help' => 'If there is only one sharing mode, it is used. Otherwise, modes are accessible by their number.',
 		'close_dropdown' => 'Close menus',
 		'close_dropdown' => 'Close menus',
 		'collapse_article' => 'Collapse',
 		'collapse_article' => 'Collapse',
 		'first_article' => 'Skip to the first article',
 		'first_article' => 'Skip to the first article',
@@ -162,7 +162,7 @@ return array(
 		'shift_for_all_read' => '+ <code>shift</code> to mark all articles as read',
 		'shift_for_all_read' => '+ <code>shift</code> to mark all articles as read',
 		'title' => 'Shortcuts',
 		'title' => 'Shortcuts',
 		'user_filter' => 'Access user filters',
 		'user_filter' => 'Access user filters',
-		'user_filter_help' => 'If there is only one user filter, it is used. Else filters are accessible by their number.',
+		'user_filter_help' => 'If there is only one user filter, it is used. Otherwise, filters are accessible by their number.',
 	),
 	),
 	'user' => array(
 	'user' => array(
 		'articles_and_size' => '%s articles (%s)',
 		'articles_and_size' => '%s articles (%s)',

+ 5 - 5
app/i18n/en/feedback.php

@@ -2,7 +2,7 @@
 
 
 return array(
 return array(
 	'admin' => array(
 	'admin' => array(
-		'optimization_complete' => 'Optimisation complete',
+		'optimization_complete' => 'Optimization complete',
 	),
 	),
 	'access' => array(
 	'access' => array(
 		'denied' => 'You don’t have permission to access this page',
 		'denied' => 'You don’t have permission to access this page',
@@ -39,26 +39,26 @@ return array(
 			'ok' => '%s is now enabled',
 			'ok' => '%s is now enabled',
 		),
 		),
 		'no_access' => 'You have no access on %s',
 		'no_access' => 'You have no access on %s',
-		'not_enabled' => '%s is not enabled yet',
+		'not_enabled' => '%s is not enabled',
 		'not_found' => '%s does not exist',
 		'not_found' => '%s does not exist',
 	),
 	),
 	'import_export' => array(
 	'import_export' => array(
 		'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
 		'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
 		'feeds_imported' => 'Your feeds have been imported and will now be updated',
 		'feeds_imported' => 'Your feeds have been imported and will now be updated',
-		'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
+		'feeds_imported_with_errors' => 'Your feeds have been imported, but some errors occurred',
 		'file_cannot_be_uploaded' => 'File cannot be uploaded!',
 		'file_cannot_be_uploaded' => 'File cannot be uploaded!',
 		'no_zip_extension' => 'ZIP extension is not present on your server.',
 		'no_zip_extension' => 'ZIP extension is not present on your server.',
 		'zip_error' => 'An error occured during ZIP import.',
 		'zip_error' => 'An error occured during ZIP import.',
 	),
 	),
 	'sub' => array(
 	'sub' => array(
-		'actualize' => 'Actualise',
+		'actualize' => 'Updating',
 		'category' => array(
 		'category' => array(
 			'created' => 'Category %s has been created.',
 			'created' => 'Category %s has been created.',
 			'deleted' => 'Category has been deleted.',
 			'deleted' => 'Category has been deleted.',
 			'emptied' => 'Category has been emptied',
 			'emptied' => 'Category has been emptied',
 			'error' => 'Category cannot be updated',
 			'error' => 'Category cannot be updated',
 			'name_exists' => 'Category name already exists.',
 			'name_exists' => 'Category name already exists.',
-			'no_id' => 'You must precise the id of the category.',
+			'no_id' => 'You must specify the id of the category.',
 			'no_name' => 'Category name cannot be empty.',
 			'no_name' => 'Category name cannot be empty.',
 			'not_delete_default' => 'You cannot delete the default category!',
 			'not_delete_default' => 'You cannot delete the default category!',
 			'not_exist' => 'The category does not exist!',
 			'not_exist' => 'The category does not exist!',

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

@@ -103,7 +103,7 @@ return array(
 	'js' => array(
 	'js' => array(
 		'category_empty' => 'Empty category',
 		'category_empty' => 'Empty category',
 		'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
 		'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
-		'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!',
+		'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favourites and user queries. It cannot be cancelled!',
 		'feedback' => array(
 		'feedback' => array(
 			'body_new_articles' => 'There are %%d new articles to read on FreshRSS.',
 			'body_new_articles' => 'There are %%d new articles to read on FreshRSS.',
 			'request_failed' => 'A request has failed, it may have been caused by Internet connection problems.',
 			'request_failed' => 'A request has failed, it may have been caused by Internet connection problems.',

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

@@ -41,7 +41,7 @@ return array(
 		'mark_cat_read' => 'Mark category as read',
 		'mark_cat_read' => 'Mark category as read',
 		'mark_feed_read' => 'Mark feed as read',
 		'mark_feed_read' => 'Mark feed as read',
 		'newer_first' => 'Newer first',
 		'newer_first' => 'Newer first',
-		'non-starred' => 'Show all but favorites',
+		'non-starred' => 'Show all but favourites',
 		'normal_view' => 'Normal view',
 		'normal_view' => 'Normal view',
 		'older_first' => 'Oldest first',
 		'older_first' => 'Oldest first',
 		'queries' => 'User queries',
 		'queries' => 'User queries',
@@ -49,7 +49,7 @@ return array(
 		'reader_view' => 'Reading view',
 		'reader_view' => 'Reading view',
 		'rss_view' => 'RSS feed',
 		'rss_view' => 'RSS feed',
 		'search_short' => 'Search',
 		'search_short' => 'Search',
-		'starred' => 'Show only favorites',
+		'starred' => 'Show only favourites',
 		'stats' => 'Statistics',
 		'stats' => 'Statistics',
 		'subscription' => 'Subscriptions management',
 		'subscription' => 'Subscriptions management',
 		'unread' => 'Show only unread',
 		'unread' => 'Show only unread',

+ 3 - 3
app/i18n/en/sub.php

@@ -10,10 +10,10 @@ return array(
 	'feed' => array(
 	'feed' => array(
 		'add' => 'Add a RSS feed',
 		'add' => 'Add a RSS feed',
 		'advanced' => 'Advanced',
 		'advanced' => 'Advanced',
-		'archiving' => 'Archivage',
+		'archiving' => 'Archiving',
 		'auth' => array(
 		'auth' => array(
 			'configuration' => 'Login',
 			'configuration' => 'Login',
-			'help' => 'Connection allows to access HTTP protected RSS feeds',
+			'help' => 'Allows access to HTTP protected RSS feeds',
 			'http' => 'HTTP Authentication',
 			'http' => 'HTTP Authentication',
 			'password' => 'HTTP password',
 			'password' => 'HTTP password',
 			'username' => 'HTTP username',
 			'username' => 'HTTP username',
@@ -22,7 +22,7 @@ return array(
 		'css_path' => 'Articles CSS path on original website',
 		'css_path' => 'Articles CSS path on original website',
 		'description' => 'Description',
 		'description' => 'Description',
 		'empty' => 'This feed is empty. Please verify that it is still maintained.',
 		'empty' => 'This feed is empty. Please verify that it is still maintained.',
-		'error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
+		'error' => 'This feed has encountered a problem. Please verify that it is always reachable then update it.',
 		'in_main_stream' => 'Show in main stream',
 		'in_main_stream' => 'Show in main stream',
 		'informations' => 'Information',
 		'informations' => 'Information',
 		'keep_history' => 'Minimum number of articles to keep',
 		'keep_history' => 'Minimum number of articles to keep',

+ 12 - 28
app/install.php

@@ -342,35 +342,19 @@ function checkDbUser(&$dbOptions) {
 	$driver_options = $dbOptions['options'];
 	$driver_options = $dbOptions['options'];
 	try {
 	try {
 		$c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options);
 		$c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options);
-
 		if (defined('SQL_CREATE_TABLES')) {
 		if (defined('SQL_CREATE_TABLES')) {
-			$sql = sprintf(SQL_CREATE_TABLES, $dbOptions['prefix_user'], _t('gen.short.default_category'));
-			$stm = $c->prepare($sql);
-			$ok = $stm->execute();
-		} else {
-			global $SQL_CREATE_TABLES;
-			if (is_array($SQL_CREATE_TABLES)) {
-				$ok = true;
-				foreach ($SQL_CREATE_TABLES as $instruction) {
-					$sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));
-					$stm = $c->prepare($sql);
-					$ok &= $stm->execute();
-				}
-			}
-		}
-
-		if (defined('SQL_INSERT_FEEDS')) {
-			$sql = sprintf(SQL_INSERT_FEEDS, $dbOptions['prefix_user']);
+			$sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_INSERT_FEEDS,
+				$dbOptions['prefix_user'], _t('gen.short.default_category'));
 			$stm = $c->prepare($sql);
 			$stm = $c->prepare($sql);
-			$ok &= $stm->execute();
+			$ok = $stm && $stm->execute();
 		} else {
 		} else {
-			global $SQL_INSERT_FEEDS;
-			if (is_array($SQL_INSERT_FEEDS)) {
-				foreach ($SQL_INSERT_FEEDS as $instruction) {
-					$sql = sprintf($instruction, $dbOptions['prefix_user']);
-					$stm = $c->prepare($sql);
-					$ok &= $stm->execute();
-				}
+			global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS;
+			$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS);
+			$ok = !empty($instructions);
+			foreach ($instructions as $instruction) {
+				$sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));
+				$stm = $c->prepare($sql);
+				$ok &= $stm && $stm->execute();
 			}
 			}
 		}
 		}
 	} catch (PDOException $e) {
 	} catch (PDOException $e) {
@@ -481,7 +465,7 @@ function printStep1() {
 	<?php if ($res['fileinfo'] == 'ok') { ?>
 	<?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>
 	<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 { ?>
 	<?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>
+	<p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.check.fileinfo.nok'); ?></p>
 	<?php } ?>
 	<?php } ?>
 
 
 	<?php if ($res['data'] == 'ok') { ?>
 	<?php if ($res['data'] == 'ok') { ?>
@@ -629,7 +613,7 @@ function printStep3() {
 				<?php if (extension_loaded('pdo_pgsql')) {?>
 				<?php if (extension_loaded('pdo_pgsql')) {?>
 				<option value="pgsql"
 				<option value="pgsql"
 					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
 					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
-					PostgreSQL (⚠️ experimental)
+					PostgreSQL
 				</option>
 				</option>
 				<?php }?>
 				<?php }?>
 				</select>
 				</select>

+ 1 - 0
app/layout/nav_menu.phtml

@@ -149,6 +149,7 @@
 		<?php
 		<?php
 			$url_output['a'] = 'rss';
 			$url_output['a'] = 'rss';
 			if (FreshRSS_Context::$user_conf->token) {
 			if (FreshRSS_Context::$user_conf->token) {
+				$url_output['params']['user'] = Minz_Session::param('currentUser');
 				$url_output['params']['token'] = FreshRSS_Context::$user_conf->token;
 				$url_output['params']['token'] = FreshRSS_Context::$user_conf->token;
 			}
 			}
 			if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
 			if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {

+ 0 - 13
app/views/auth/index.phtml

@@ -52,19 +52,6 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<?php if (FreshRSS_Auth::accessNeedsAction()) { ?>
-		<div class="form-group">
-			<label class="group-name" for="token"><?php echo _t('admin.auth.token'); ?></label>
-			<?php $token = FreshRSS_Context::$user_conf->token; ?>
-			<div class="group-controls">
-				<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo _t('gen.short.blank_to_disable'); ?>"<?php
-					echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?php echo $token; ?>"/>
-				<?php echo _i('help'); ?> <?php echo _t('admin.auth.token_help'); ?>
-				<kbd><?php echo Minz_Url::display(array('a' => 'rss', 'params' => array('token' => $token, 'hours' => FreshRSS_Context::$user_conf->since_hours_posts_per_rss)), 'html', true); ?></kbd>
-			</div>
-		</div>
-		<?php } ?>
-
 		<div class="form-group">
 		<div class="form-group">
 			<div class="group-controls">
 			<div class="group-controls">
 				<label class="checkbox" for="api_enabled">
 				<label class="checkbox" for="api_enabled">

+ 13 - 0
app/views/user/profile.phtml

@@ -43,6 +43,19 @@
 		</div>
 		</div>
 		<?php } ?>
 		<?php } ?>
 
 
+		<?php if (FreshRSS_Auth::accessNeedsAction()) { ?>
+		<div class="form-group">
+			<label class="group-name" for="token"><?php echo _t('admin.auth.token'); ?></label>
+			<?php $token = FreshRSS_Context::$user_conf->token; ?>
+			<div class="group-controls">
+				<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo _t('gen.short.blank_to_disable'); ?>"<?php
+					echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?php echo $token; ?>"/>
+				<?php echo _i('help'); ?> <?php echo _t('admin.auth.token_help'); ?>
+				<kbd><?php echo Minz_Url::display(array('a' => 'rss', 'params' => array('user' => Minz_Session::param('currentUser'), 'token' => $token, 'hours' => FreshRSS_Context::$user_conf->since_hours_posts_per_rss)), 'html', true); ?></kbd>
+			</div>
+		</div>
+		<?php } ?>
+
 		<div class="form-group form-actions">
 		<div class="form-group form-actions">
 			<div class="group-controls">
 			<div class="group-controls">
 				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
 				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>

+ 1 - 1
constants.php

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

+ 0 - 41
lib/Favicon/DataAccess.php

@@ -1,41 +0,0 @@
-<?php
-
-namespace Favicon;
-
-/**
- * DataAccess is a wrapper used to read/write data locally or remotly
- * Aside from SOLID principles, this wrapper is also useful to mock remote resources in unit tests
- * Note: remote access warning are silenced because we don't care if a website is unreachable
- **/
-class DataAccess {
-	public function retrieveUrl($url) {
-		$this->set_context();
-		return @file_get_contents($url);
-	}
-
-	public function retrieveHeader($url) {
-		$this->set_context();
-		$headers = @get_headers($url, 1);
-		return is_array($headers) ? array_change_key_case($headers) : array();
-	}
-
-	public function saveCache($file, $data) {
-		file_put_contents($file, $data);
-	}
-
-	public function readCache($file) {
-		return file_get_contents($file);
-	}
-
-	private function set_context() {
-		stream_context_set_default(
-			array(
-				'http' => array(
-					'method' => 'GET',
-					'timeout' => 10,
-					'header' => "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:20.0; Favicon; +https://github.com/ArthurHoaro/favicon) Gecko/20100101 Firefox/32.0\r\n",
-				)
-			)
-		);
-	}
-}

+ 0 - 296
lib/Favicon/Favicon.php

@@ -1,296 +0,0 @@
-<?php
-
-namespace Favicon;
-
-class Favicon
-{
-    protected $url = '';
-    protected $cacheDir;
-    protected $cacheTimeout;
-    protected $dataAccess;
-
-    public function __construct($args = array())
-    {
-        if (isset($args['url'])) {
-            $this->url = $args['url'];
-        }
-        
-        $this->cacheDir = __DIR__ . '/../../resources/cache';
-        $this->dataAccess = new DataAccess();
-    }
-
-    public function cache($args = array()) {
-        if (isset($args['dir'])) {
-            $this->cacheDir = $args['dir'];
-        }
-
-        if (!empty($args['timeout'])) {
-                $this->cacheTimeout = $args['timeout'];
-        } else {
-                $this->cacheTimeout = 0;
-        }
-    }
-
-    public static function baseUrl($url, $path = false)
-    {
-        $return = '';
-
-        if (!$url = parse_url($url)) {
-            return FALSE;
-        }
-
-        // Scheme
-        $scheme = isset($url['scheme']) ? strtolower($url['scheme']) : null;
-        if ($scheme != 'http' && $scheme != 'https') {
-
-            return FALSE;
-        }
-        $return .= "{$scheme}://";
-
-        // Username and password
-        if (isset($url['user'])) {
-            $return .= $url['user'];
-            if (isset($url['pass'])) {
-                $return .= ":{$url['pass']}";
-            }
-            $return .= '@';
-        }
-
-        // Hostname
-        if( !isset($url['host']) ) {
-            return FALSE;
-        }
-        
-        $return .= $url['host'];
-
-        // Port
-        if (isset($url['port'])) {
-            $return .= ":{$url['port']}";
-        }
-
-        // Path
-        if( $path && isset($url['path']) ) {
-            $return .= $url['path'];
-        }
-        $return .= '/';
-
-        return $return;    
-    }
-
-    public function info($url)
-    {
-        if(empty($url) || $url === false) {
-            return false;
-        }
-        
-        $max_loop = 5;
-        
-        // Discover real status by following redirects. 
-        $loop = TRUE;
-        while ($loop && $max_loop-- > 0) {
-            $headers = $this->dataAccess->retrieveHeader($url);
-            if (empty($headers)) {
-                return false;
-            }
-            $exploded = explode(' ', $headers[0]);
-            
-            if( !isset($exploded[1]) ) { 
-                return false;
-            }
-            list(,$status) = $exploded;
-            
-            switch ($status) {
-                case '301':
-                case '302':
-                    $url = isset($headers['location']) ? $headers['location'] : '';
-                    break;
-                default:
-                    $loop = FALSE;
-                    break;
-            }
-        }
-
-        return array('status' => $status, 'url' => $url);
-    }
-    
-    public function endRedirect($url) {
-        $out = $this->info($url);
-        return !empty($out['url']) ? $out['url'] : false;
-    }
-
-    /**
-     * Find remote (or cached) favicon
-     * @return favicon URL, false if nothing was found
-     **/
-    public function get($url = '')
-    {
-        // URLs passed to this method take precedence.
-        if (!empty($url)) {
-            $this->url = $url;
-        }
-
-        // Get the base URL without the path for clearer concatenations.
-        $original = rtrim($this->baseUrl($this->url, true), '/');
-        $url = rtrim($this->endRedirect($this->baseUrl($this->url, false)), '/');
-
-        if(($favicon = $this->checkCache($url)) || ($favicon = $this->getFavicon($url))) {
-            $base = true;
-        }
-        elseif(($favicon = $this->checkCache($original)) || ($favicon = $this->getFavicon($original, false))) {
-            $base = false;    
-        }
-        else
-            return false;
-            
-        // Save cache if necessary
-        $cache = $this->cacheDir . '/' . md5($base ? $url : $original);
-        if ($this->cacheTimeout && !file_exists($cache) || (is_writable($cache) && time() - filemtime($cache) > $this->cacheTimeout)) {
-            $this->dataAccess->saveCache($cache, $favicon);
-        }
-        
-        return $favicon;
-    }
-    
-    private function getFavicon($url, $checkDefault = true) {
-        $favicon = false;
-        
-        if(empty($url)) {
-            return false;
-        }
-        
-        // Try /favicon.ico first.
-        if( $checkDefault ) {
-            $info = $this->info("{$url}/favicon.ico");
-            if ($info['status'] == '200') {
-                $favicon = $info['url'];
-            }
-        }
-
-        // See if it's specified in a link tag in domain url.
-        if (!$favicon) {
-            $favicon = $this->getInPage($url);
-        }
-        
-        // Make sure the favicon is an absolute URL.
-        if( $favicon && filter_var($favicon, FILTER_VALIDATE_URL) === false ) {
-            $favicon = $url . '/' . $favicon;
-        }
-
-        // Sometimes people lie, so check the status.
-        // And sometimes, it's not even an image. Sneaky bastards!
-        // If cacheDir isn't writable, that's not our problem
-        if ($favicon && is_writable($this->cacheDir) && !$this->checkImageMType($favicon)) {
-            $favicon = false;
-        }
-
-        return $favicon;
-    }
-    
-    private function getInPage($url) {
-        $html = $this->dataAccess->retrieveUrl("{$url}/");
-        preg_match('!<head.*?>.*</head>!ims', $html, $match);
-        
-        if(empty($match) || count($match) == 0) {
-            return false;
-        }
-        
-        $head = $match[0];
-        
-        $dom = new \DOMDocument();
-        // Use error supression, because the HTML might be too malformed.
-        if (@$dom->loadHTML($head)) {
-            $links = $dom->getElementsByTagName('link');
-            foreach ($links as $link) {
-                if ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'shortcut icon') {
-                    return $link->getAttribute('href');
-                } elseif ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'icon') {
-                    return $link->getAttribute('href');
-                } elseif ($link->hasAttribute('href') && strpos($link->getAttribute('href'), 'favicon') !== FALSE) {
-                    return $link->getAttribute('href');
-                }
-            }
-        }
-        return false;
-    }
-    
-    private function checkCache($url) {
-        if ($this->cacheTimeout) {
-            $cache = $this->cacheDir . '/' . md5($url);
-            if (file_exists($cache) && is_readable($cache) && (time() - filemtime($cache) < $this->cacheTimeout)) {
-                return $this->dataAccess->readCache($cache);
-            }
-        } 
-        return false;
-    }
-    
-    private function checkImageMType($url) {
-        $tmpFile = $this->cacheDir . '/tmp.ico';
-        
-        $fileContent = $this->dataAccess->retrieveUrl($url);
-        $this->dataAccess->saveCache($tmpFile, $fileContent);
-        
-        $finfo = finfo_open(FILEINFO_MIME_TYPE);
-        $isImage = strpos(finfo_file($finfo, $tmpFile), 'image') !== false;
-        finfo_close($finfo);
-        
-        unlink($tmpFile);
-        
-        return $isImage;
-    }
-    
-    /**
-     * @return mixed
-     */
-    public function getCacheDir()
-    {
-        return $this->cacheDir;
-    }
-
-    /**
-     * @param mixed $cacheDir
-     */
-    public function setCacheDir($cacheDir)
-    {
-        $this->cacheDir = $cacheDir;
-    }
-
-    /**
-     * @return mixed
-     */
-    public function getCacheTimeout()
-    {
-        return $this->cacheTimeout;
-    }
-
-    /**
-     * @param mixed $cacheTimeout
-     */
-    public function setCacheTimeout($cacheTimeout)
-    {
-        $this->cacheTimeout = $cacheTimeout;
-    }
-
-    /**
-     * @return string
-     */
-    public function getUrl()
-    {
-        return $this->url;
-    }
-
-    /**
-     * @param string $url
-     */
-    public function setUrl($url)
-    {
-        $this->url = $url;
-    }
-
-    /**
-     * @param DataAccess $dataAccess
-     */
-    public function setDataAccess($dataAccess)
-    {
-        $this->dataAccess = $dataAccess;
-    }
-}

+ 51 - 35
lib/SimplePie/SimplePie.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -34,7 +34,7 @@
  *
  *
  * @package SimplePie
  * @package SimplePie
  * @version 1.4-dev-FreshRSS
  * @version 1.4-dev-FreshRSS
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -614,6 +614,12 @@ class SimplePie
 	 */
 	 */
 	public $item_limit = 0;
 	public $item_limit = 0;
 
 
+	/**
+	 * @var bool Stores if last-modified and/or etag headers were sent with the
+	 * request when checking a feed.
+	 */
+	public $check_modified = false;
+
 	/**
 	/**
 	 * @var array Stores the default attributes to be stripped by strip_attributes().
 	 * @var array Stores the default attributes to be stripped by strip_attributes().
 	 * @see SimplePie::strip_attributes()
 	 * @see SimplePie::strip_attributes()
@@ -626,7 +632,7 @@ class SimplePie
 	 * @see SimplePie::add_attributes()
 	 * @see SimplePie::add_attributes()
 	 * @access private
 	 * @access private
 	 */
 	 */
-	public $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none'));	//FreshRSS
+	public $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none'));
 
 
 	/**
 	/**
 	 * @var array Stores the default tags to be stripped by strip_htmltags().
 	 * @var array Stores the default tags to be stripped by strip_htmltags().
@@ -657,9 +663,9 @@ class SimplePie
 	 */
 	 */
 	public function __construct()
 	public function __construct()
 	{
 	{
-		if (version_compare(PHP_VERSION, '5.2', '<'))
+		if (version_compare(PHP_VERSION, '5.3', '<'))
 		{
 		{
-			trigger_error('PHP 4.x, 5.0 and 5.1 are no longer supported. Please upgrade to PHP 5.2 or newer.');
+			trigger_error('Please upgrade to PHP 5.3 or newer.');
 			die();
 			die();
 		}
 		}
 
 
@@ -814,7 +820,7 @@ class SimplePie
 	{
 	{
 		$this->timeout = (int) $timeout;
 		$this->timeout = (int) $timeout;
 	}
 	}
-    
+
 	/**
 	/**
 	 * Set custom curl options
 	 * Set custom curl options
 	 *
 	 *
@@ -1169,7 +1175,7 @@ class SimplePie
 		$this->sanitize->strip_attributes($attribs);
 		$this->sanitize->strip_attributes($attribs);
 	}
 	}
 
 
-	public function add_attributes($attribs = '')	//FreshRSS
+	public function add_attributes($attribs = '')
 	{
 	{
 		if ($attribs === '')
 		if ($attribs === '')
 		{
 		{
@@ -1191,11 +1197,11 @@ class SimplePie
 	 *
 	 *
 	 * Allows you to override SimplePie's output to match that of your webpage.
 	 * Allows you to override SimplePie's output to match that of your webpage.
 	 * This is useful for times when your webpages are not being served as
 	 * This is useful for times when your webpages are not being served as
-	 * UTF-8.  This setting will be obeyed by {@see handle_content_type()}, and
+	 * UTF-8. This setting will be obeyed by {@see handle_content_type()}, and
 	 * is similar to {@see set_input_encoding()}.
 	 * is similar to {@see set_input_encoding()}.
 	 *
 	 *
 	 * It should be noted, however, that not all character encodings can support
 	 * It should be noted, however, that not all character encodings can support
-	 * all characters.  If your page is being served as ISO-8859-1 and you try
+	 * all characters. If your page is being served as ISO-8859-1 and you try
 	 * to display a Japanese feed, you'll likely see garbled characters.
 	 * to display a Japanese feed, you'll likely see garbled characters.
 	 * Because of this, it is highly recommended to ensure that your webpages
 	 * Because of this, it is highly recommended to ensure that your webpages
 	 * are served as UTF-8.
 	 * are served as UTF-8.
@@ -1293,7 +1299,7 @@ class SimplePie
 	/**
 	/**
 	 * Initialize the feed object
 	 * Initialize the feed object
 	 *
 	 *
-	 * This is what makes everything happen.  Period.  This is where all of the
+	 * This is what makes everything happen. Period. This is where all of the
 	 * configuration options get processed, feeds are fetched, cached, and
 	 * configuration options get processed, feeds are fetched, cached, and
 	 * parsed, and all of that other good stuff.
 	 * parsed, and all of that other good stuff.
 	 *
 	 *
@@ -1361,6 +1367,7 @@ class SimplePie
 
 
 		$this->error = null;
 		$this->error = null;
 		$this->data = array();
 		$this->data = array();
+		$this->check_modified = false;
 		$this->multifeed_objects = array();
 		$this->multifeed_objects = array();
 		$cache = false;
 		$cache = false;
 
 
@@ -1380,6 +1387,7 @@ class SimplePie
 				return $this->data['mtime'];
 				return $this->data['mtime'];
 			}
 			}
 			elseif ($fetched === false) {
 			elseif ($fetched === false) {
+				$this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__));
 				return false;
 				return false;
 			}
 			}
 
 
@@ -1486,11 +1494,19 @@ class SimplePie
 		if (isset($parser))
 		if (isset($parser))
 		{
 		{
 			// We have an error, just set SimplePie_Misc::error to it and quit
 			// We have an error, just set SimplePie_Misc::error to it and quit
-			$this->error = sprintf('This XML document is invalid, likely due to invalid characters. XML error: %s at line %d, column %d, encoding %s, URL: %s', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column(), $encoding, $this->feed_url);
+			$this->error = $this->feed_url;
+			$this->error .= sprintf(' is invalid XML, 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());
 		}
 		}
 		else
 		else
 		{
 		{
-			$this->error = 'The data could not be converted to UTF-8. You MUST have either the iconv or mbstring extension installed. Upgrading to PHP 5.x (which includes iconv) is highly recommended.';
+			$this->error = 'The data could not be converted to UTF-8.';
+			if (!extension_loaded('mbstring') && !extension_loaded('iconv')) {
+				$this->error .= ' You MUST have either the iconv or mbstring extension installed.';
+			} elseif (!extension_loaded('mbstring')) {
+				$this->error .= ' Try installing the mbstring extension.';
+			} elseif (!extension_loaded('iconv')) {
+				$this->error .= ' Try installing the iconv extension.';
+			}
 		}
 		}
 
 
 		$this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__));
 		$this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__));
@@ -1575,6 +1591,7 @@ class SimplePie
 					}
 					}
 					else
 					else
 					{
 					{
+						$this->check_modified = false;
 						$cache->touch();
 						$cache->touch();
 						$this->error = $file->error;
 						$this->error = $file->error;
 						return !empty($this->data);
 						return !empty($this->data);
@@ -1669,7 +1686,7 @@ class SimplePie
 			$locate = null;
 			$locate = null;
 		}
 		}
 
 
-		$file->body = trim($file->body);
+		$file->body = trim($file->body);	//FreshRSS
 		$this->raw_data = $file->body;
 		$this->raw_data = $file->body;
 		$this->permanent_url = $file->permanent_url;
 		$this->permanent_url = $file->permanent_url;
 		$headers = $file->headers;
 		$headers = $file->headers;
@@ -1870,7 +1887,7 @@ class SimplePie
 	 * @todo Support <itunes:new-feed-url>
 	 * @todo Support <itunes:new-feed-url>
 	 * @todo Also, |atom:link|@rel=self
 	 * @todo Also, |atom:link|@rel=self
 	 * @param bool $permanent Permanent mode to return only the original URL or the first redirection
 	 * @param bool $permanent Permanent mode to return only the original URL or the first redirection
-	 *  iff it is a 301 redirection
+	 * iff it is a 301 redirection
 	 * @return string|null
 	 * @return string|null
 	 */
 	 */
 	public function subscribe_url($permanent = false)
 	public function subscribe_url($permanent = false)
@@ -2169,7 +2186,7 @@ class SimplePie
 	 * Get a category for the feed
 	 * Get a category for the feed
 	 *
 	 *
 	 * @since Unknown
 	 * @since Unknown
-	 * @param int $key The category that you want to return.  Remember that arrays begin with 0, not 1
+	 * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1
 	 * @return SimplePie_Category|null
 	 * @return SimplePie_Category|null
 	 */
 	 */
 	public function get_category($key = 0)
 	public function get_category($key = 0)
@@ -2254,7 +2271,7 @@ class SimplePie
 	 * Get an author for the feed
 	 * Get an author for the feed
 	 *
 	 *
 	 * @since 1.1
 	 * @since 1.1
-	 * @param int $key The author that you want to return.  Remember that arrays begin with 0, not 1
+	 * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1
 	 * @return SimplePie_Author|null
 	 * @return SimplePie_Author|null
 	 */
 	 */
 	public function get_author($key = 0)
 	public function get_author($key = 0)
@@ -2352,7 +2369,7 @@ class SimplePie
 	 * Get a contributor for the feed
 	 * Get a contributor for the feed
 	 *
 	 *
 	 * @since 1.1
 	 * @since 1.1
-	 * @param int $key The contrbutor that you want to return.  Remember that arrays begin with 0, not 1
+	 * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1
 	 * @return SimplePie_Author|null
 	 * @return SimplePie_Author|null
 	 */
 	 */
 	public function get_contributor($key = 0)
 	public function get_contributor($key = 0)
@@ -2438,7 +2455,7 @@ class SimplePie
 	 * Get a single link for the feed
 	 * Get a single link for the feed
 	 *
 	 *
 	 * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8)
 	 * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8)
-	 * @param int $key The link that you want to return.  Remember that arrays begin with 0, not 1
+	 * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1
 	 * @param string $rel The relationship of the link to return
 	 * @param string $rel The relationship of the link to return
 	 * @return string|null Link URL
 	 * @return string|null Link URL
 	 */
 	 */
@@ -2949,7 +2966,7 @@ class SimplePie
 	 *
 	 *
 	 * @see get_item_quantity()
 	 * @see get_item_quantity()
 	 * @since Beta 2
 	 * @since Beta 2
-	 * @param int $key The item that you want to return.  Remember that arrays begin with 0, not 1
+	 * @param int $key The item that you want to return. Remember that arrays begin with 0, not 1
 	 * @return SimplePie_Item|null
 	 * @return SimplePie_Item|null
 	 */
 	 */
 	public function get_item($key = 0)
 	public function get_item($key = 0)
@@ -2976,7 +2993,7 @@ class SimplePie
 	 * @since Beta 2
 	 * @since Beta 2
 	 * @param int $start Index to start at
 	 * @param int $start Index to start at
 	 * @param int $end Number of items to return. 0 for all items after `$start`
 	 * @param int $end Number of items to return. 0 for all items after `$start`
-	 * @return array|null List of {@see SimplePie_Item} objects
+	 * @return SimplePie_Item[]|null List of {@see SimplePie_Item} objects
 	 */
 	 */
 	public function get_items($start = 0, $end = 0)
 	public function get_items($start = 0, $end = 0)
 	{
 	{
@@ -3147,7 +3164,19 @@ class SimplePie
 	 */
 	 */
 	public static function sort_items($a, $b)
 	public static function sort_items($a, $b)
 	{
 	{
-		return $a->get_date('U') <= $b->get_date('U');
+		$a_date = $a->get_date('U');
+		$b_date = $b->get_date('U');
+		if ($a_date && $b_date) {
+			return $a_date > $b_date ? -1 : 1;
+		}
+		// Sort items without dates to the top.
+		if ($a_date) {
+			return 1;
+		}
+		if ($b_date) {
+			return -1;
+		}
+		return 0;
 	}
 	}
 
 
 	/**
 	/**
@@ -3180,20 +3209,7 @@ class SimplePie
 				}
 				}
 			}
 			}
 
 
-			$do_sort = true;
-			foreach ($items as $item)
-			{
-				if (!$item->get_date('U'))
-				{
-					$do_sort = false;
-					break;
-				}
-			}
-			$item = null;
-			if ($do_sort)
-			{
-				usort($items, array(get_class($urls[0]), 'sort_items'));
-			}
+			usort($items, array(get_class($urls[0]), 'sort_items'));
 
 
 			if ($end === 0)
 			if ($end === 0)
 			{
 			{

+ 2 - 3
lib/SimplePie/SimplePie/Author.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 6 - 5
lib/SimplePie/SimplePie/Cache.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -62,8 +61,10 @@ class SimplePie_Cache
 	 * @var array
 	 * @var array
 	 */
 	 */
 	protected static $handlers = array(
 	protected static $handlers = array(
-		'mysql' => 'SimplePie_Cache_MySQL',
-		'memcache' => 'SimplePie_Cache_Memcache',
+		'mysql'     => 'SimplePie_Cache_MySQL',
+		'memcache'  => 'SimplePie_Cache_Memcache',
+		'memcached' => 'SimplePie_Cache_Memcached',
+		'redis'     => 'SimplePie_Cache_Redis'
 	);
 	);
 
 
 	/**
 	/**

+ 2 - 3
lib/SimplePie/SimplePie/Cache/Base.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Cache/DB.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

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

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Cache/Memcache.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 166 - 0
lib/SimplePie/SimplePie/Cache/Memcached.php

@@ -0,0 +1,166 @@
+<?php
+/**
+ * SimplePie
+ *
+ * A PHP-Based RSS and Atom Feed Framework.
+ * Takes the hard work out of managing a complete RSS/Atom solution.
+ *
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 	* Redistributions of source code must retain the above copyright notice, this list of
+ * 	  conditions and the following disclaimer.
+ *
+ * 	* Redistributions in binary form must reproduce the above copyright notice, this list
+ * 	  of conditions and the following disclaimer in the documentation and/or other materials
+ * 	  provided with the distribution.
+ *
+ * 	* Neither the name of the SimplePie Team nor the names of its contributors may be used
+ * 	  to endorse or promote products derived from this software without specific prior
+ * 	  written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS
+ * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @package SimplePie
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @author Ryan Parman
+ * @author Geoffrey Sneddon
+ * @author Ryan McCue
+ * @link http://simplepie.org/ SimplePie
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License
+ */
+
+/**
+ * Caches data to memcached
+ *
+ * Registered for URLs with the "memcached" protocol
+ *
+ * For example, `memcached://localhost:11211/?timeout=3600&prefix=sp_` will
+ * connect to memcached on `localhost` on port 11211. All tables will be
+ * prefixed with `sp_` and data will expire after 3600 seconds
+ *
+ * @package    SimplePie
+ * @subpackage Caching
+ * @author     Paul L. McNeely
+ * @uses       Memcached
+ */
+class SimplePie_Cache_Memcached implements SimplePie_Cache_Base
+{
+    /**
+     * Memcached instance
+     * @var Memcached
+     */
+    protected $cache;
+
+    /**
+     * Options
+     * @var array
+     */
+    protected $options;
+
+    /**
+     * Cache name
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * Create a new cache object
+     * @param string $location Location string (from SimplePie::$cache_location)
+     * @param string $name     Unique ID for the cache
+     * @param string $type     Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data
+     */
+    public function __construct($location, $name, $type) {
+        $this->options = array(
+            'host'   => '127.0.0.1',
+            'port'   => 11211,
+            'extras' => array(
+                'timeout' => 3600, // one hour
+                'prefix'  => 'simplepie_',
+            ),
+        );
+        $this->options = SimplePie_Misc::array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location));
+
+        $this->name = $this->options['extras']['prefix'] . md5("$name:$type");
+
+        $this->cache = new Memcached();
+        $this->cache->addServer($this->options['host'], (int)$this->options['port']);
+    }
+
+    /**
+     * Save data to the cache
+     * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property
+     * @return bool Successfulness
+     */
+    public function save($data) {
+        if ($data instanceof SimplePie) {
+            $data = $data->data;
+        }
+
+        return $this->setData(serialize($data));
+    }
+
+    /**
+     * Retrieve the data saved to the cache
+     * @return array Data for SimplePie::$data
+     */
+    public function load() {
+        $data = $this->cache->get($this->name);
+
+        if ($data !== false) {
+            return unserialize($data);
+        }
+        return false;
+    }
+
+    /**
+     * Retrieve the last modified time for the cache
+     * @return int Timestamp
+     */
+    public function mtime() {
+        $data = $this->cache->get($this->name . '_mtime');
+        return (int) $data;
+    }
+
+    /**
+     * Set the last modified time to the current time
+     * @return bool Success status
+     */
+    public function touch() {
+        $data = $this->cache->get($this->name);
+        return $this->setData($data);
+    }
+
+    /**
+     * Remove the cache
+     * @return bool Success status
+     */
+    public function unlink() {
+        return $this->cache->delete($this->name, 0);
+    }
+
+    /**
+     * Set the last modified time and data to Memcached
+     * @return bool Success status
+     */
+    private function setData($data) {
+
+        if ($data !== false) {
+            $this->cache->set($this->name . '_mtime', time(), (int)$this->options['extras']['timeout']);
+            return $this->cache->set($this->name, $data, (int)$this->options['extras']['timeout']);
+        }
+
+        return false;
+    }
+}

+ 19 - 4
lib/SimplePie/SimplePie/Cache/MySQL.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -94,6 +93,7 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB
 			'path' => '',
 			'path' => '',
 			'extras' => array(
 			'extras' => array(
 				'prefix' => '',
 				'prefix' => '',
+				'cache_purge_time' => 2592000
 			),
 			),
 		);
 		);
 		
 		
@@ -131,16 +131,20 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB
 			$query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'cache_data` (`id` TEXT CHARACTER SET utf8 NOT NULL, `items` SMALLINT NOT NULL DEFAULT 0, `data` BLOB NOT NULL, `mtime` INT UNSIGNED NOT NULL, UNIQUE (`id`(125)))');
 			$query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'cache_data` (`id` TEXT CHARACTER SET utf8 NOT NULL, `items` SMALLINT NOT NULL DEFAULT 0, `data` BLOB NOT NULL, `mtime` INT UNSIGNED NOT NULL, UNIQUE (`id`(125)))');
 			if ($query === false)
 			if ($query === false)
 			{
 			{
+				trigger_error("Can't create " . $this->options['extras']['prefix'] . "cache_data table, check permissions", E_USER_WARNING);
 				$this->mysql = null;
 				$this->mysql = null;
+				return;
 			}
 			}
 		}
 		}
 
 
 		if (!in_array($this->options['extras']['prefix'] . 'items', $db))
 		if (!in_array($this->options['extras']['prefix'] . 'items', $db))
 		{
 		{
-			$query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` MEDIUMBLOB CHARACTER SET utf8 NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))');
+			$query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` MEDIUMBLOB NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))');
 			if ($query === false)
 			if ($query === false)
 			{
 			{
+				trigger_error("Can't create " . $this->options['extras']['prefix'] . "items table, check permissions", E_USER_WARNING);
 				$this->mysql = null;
 				$this->mysql = null;
+				return;
 			}
 			}
 		}
 		}
 	}
 	}
@@ -158,6 +162,17 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB
 			return false;
 			return false;
 		}
 		}
 
 
+		$query = $this->mysql->prepare('DELETE i, cd FROM `' . $this->options['extras']['prefix'] . 'cache_data` cd, ' .
+			'`' . $this->options['extras']['prefix'] . 'items` i ' .
+			'WHERE cd.id = i.feed_id ' .
+			'AND cd.mtime < (unix_timestamp() - :purge_time)');
+		$query->bindValue(':purge_time', $this->options['extras']['cache_purge_time']);
+
+		if (!$query->execute())
+		{
+			return false;
+		}
+
 		if ($data instanceof SimplePie)
 		if ($data instanceof SimplePie)
 		{
 		{
 			$data = clone $data;
 			$data = clone $data;

+ 166 - 0
lib/SimplePie/SimplePie/Cache/Redis.php

@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * SimplePie Redis Cache Extension
+ *
+ * @package SimplePie
+ * @author Jan Kozak <galvani78@gmail.com>
+ * @link http://galvani.cz/
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License
+ * @version 0.2.9
+ */
+
+
+/**
+ * Caches data to redis
+ *
+ * Registered for URLs with the "redis" protocol
+ *
+ * For example, `redis://localhost:6379/?timeout=3600&prefix=sp_&dbIndex=0` will
+ * connect to redis on `localhost` on port 6379. All tables will be
+ * prefixed with `simple_primary-` and data will expire after 3600 seconds
+ *
+ * @package SimplePie
+ * @subpackage Caching
+ * @uses Redis
+ */
+class SimplePie_Cache_Redis implements SimplePie_Cache_Base {
+    /**
+     * Redis instance
+     *
+     * @var \Redis
+     */
+    protected $cache;
+
+    /**
+     * Options
+     *
+     * @var array
+     */
+    protected $options;
+
+    /**
+     * Cache name
+     *
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * Cache Data
+     *
+     * @var type
+     */
+    protected $data;
+
+    /**
+     * Create a new cache object
+     *
+     * @param string $location Location string (from SimplePie::$cache_location)
+     * @param string $name Unique ID for the cache
+     * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data
+     */
+    public function __construct($location, $name, $options = null) {
+        //$this->cache = \flow\simple\cache\Redis::getRedisClientInstance();
+        $parsed = SimplePie_Cache::parse_URL($location);
+        $redis = new Redis();
+        $redis->connect($parsed['host'], $parsed['port']);
+        $this->cache = $redis;
+
+        if (!is_null($options) && is_array($options)) {
+            $this->options = $options;
+        } else {
+            $this->options = array (
+                'prefix' => 'rss:simple_primary:',
+                'expire' => 0,
+            );
+        }
+
+        $this->name = $this->options['prefix'] . $name;
+    }
+
+    /**
+     * @param \Redis $cache
+     */
+    public function setRedisClient(\Redis $cache) {
+        $this->cache = $cache;
+    }
+
+    /**
+     * Save data to the cache
+     *
+     * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property
+     * @return bool Successfulness
+     */
+    public function save($data) {
+        if ($data instanceof SimplePie) {
+            $data = $data->data;
+        }
+        $response = $this->cache->set($this->name, serialize($data));
+        if ($this->options['expire']) {
+            $this->cache->expire($this->name, $this->options['expire']);
+        }
+
+        return $response;
+    }
+
+    /**
+     * Retrieve the data saved to the cache
+     *
+     * @return array Data for SimplePie::$data
+     */
+    public function load() {
+        $data = $this->cache->get($this->name);
+
+        if ($data !== false) {
+            return unserialize($data);
+        }
+        return false;
+    }
+
+    /**
+     * Retrieve the last modified time for the cache
+     *
+     * @return int Timestamp
+     */
+    public function mtime() {
+
+        $data = $this->cache->get($this->name);
+
+        if ($data !== false) {
+            return time();
+        }
+
+        return false;
+    }
+
+    /**
+     * Set the last modified time to the current time
+     *
+     * @return bool Success status
+     */
+    public function touch() {
+
+        $data = $this->cache->get($this->name);
+
+        if ($data !== false) {
+            $return = $this->cache->set($this->name, $data);
+            if ($this->options['expire']) {
+                return $this->cache->expire($this->name, $this->ttl);
+            }
+            return $return;
+        }
+
+        return false;
+    }
+
+    /**
+     * Remove the cache
+     *
+     * @return bool Success status
+     */
+    public function unlink() {
+        return $this->cache->set($this->name, null);
+    }
+
+}

+ 2 - 3
lib/SimplePie/SimplePie/Caption.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Category.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Content/Type/Sniffer.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Copyright.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Core.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2009, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Credit.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Decode/HTML/Entities.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 3 - 4
lib/SimplePie/SimplePie/Enclosure.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -451,7 +450,7 @@ class SimplePie_Enclosure
 	/**
 	/**
 	 * Get the duration of the enclosure
 	 * Get the duration of the enclosure
 	 *
 	 *
-	 * @param string $convert Convert seconds into hh:mm:ss
+	 * @param bool $convert Convert seconds into hh:mm:ss
 	 * @return string|int|null 'hh:mm:ss' string if `$convert` was specified, otherwise integer (or null if none found)
 	 * @return string|int|null 'hh:mm:ss' string if `$convert` was specified, otherwise integer (or null if none found)
 	 */
 	 */
 	public function get_duration($convert = false)
 	public function get_duration($convert = false)

+ 2 - 3
lib/SimplePie/SimplePie/Exception.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

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

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -64,7 +63,7 @@ class SimplePie_File
 	var $redirects = 0;
 	var $redirects = 0;
 	var $error;
 	var $error;
 	var $method = SIMPLEPIE_FILE_SOURCE_NONE;
 	var $method = SIMPLEPIE_FILE_SOURCE_NONE;
-	var $permanent_url;	//FreshRSS
+	var $permanent_url;
 
 
 	public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $curl_options = array(), $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)
 	{
 	{
@@ -108,6 +107,7 @@ class SimplePie_File
 				curl_setopt($fp, CURLOPT_URL, $url);
 				curl_setopt($fp, CURLOPT_URL, $url);
 				curl_setopt($fp, CURLOPT_HEADER, 1);
 				curl_setopt($fp, CURLOPT_HEADER, 1);
 				curl_setopt($fp, CURLOPT_RETURNTRANSFER, 1);
 				curl_setopt($fp, CURLOPT_RETURNTRANSFER, 1);
+				curl_setopt($fp, CURLOPT_FAILONERROR, 1);
 				curl_setopt($fp, CURLOPT_TIMEOUT, $timeout);
 				curl_setopt($fp, CURLOPT_TIMEOUT, $timeout);
 				curl_setopt($fp, CURLOPT_CONNECTTIMEOUT, $timeout);
 				curl_setopt($fp, CURLOPT_CONNECTTIMEOUT, $timeout);
 				curl_setopt($fp, CURLOPT_REFERER, $url);
 				curl_setopt($fp, CURLOPT_REFERER, $url);
@@ -144,7 +144,7 @@ class SimplePie_File
 					if ($parser->parse())
 					if ($parser->parse())
 					{
 					{
 						$this->headers = $parser->headers;
 						$this->headers = $parser->headers;
-						$this->body = $parser->body;
+						$this->body = trim($parser->body);
 						$this->status_code = $parser->status_code;
 						$this->status_code = $parser->status_code;
 						if ((in_array($this->status_code, array(300, 301, 302, 303, 307)) || $this->status_code > 307 && $this->status_code < 400) && isset($this->headers['location']) && $this->redirects < $redirects)
 						if ((in_array($this->status_code, array(300, 301, 302, 303, 307)) || $this->status_code > 307 && $this->status_code < 400) && isset($this->headers['location']) && $this->redirects < $redirects)
 						{
 						{
@@ -237,7 +237,7 @@ class SimplePie_File
 								$location = SimplePie_Misc::absolutize_url($this->headers['location'], $url);
 								$location = SimplePie_Misc::absolutize_url($this->headers['location'], $url);
 								$previousStatusCode = $this->status_code;
 								$previousStatusCode = $this->status_code;
 								$this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
 								$this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen);
-								$this->permanent_url = ($previousStatusCode == 301) ? $location : $url;	//FreshRSS
+								$this->permanent_url = ($previousStatusCode == 301) ? $location : $url;
 								return;
 								return;
 							}
 							}
 							if (isset($this->headers['content-encoding']))
 							if (isset($this->headers['content-encoding']))
@@ -255,7 +255,7 @@ class SimplePie_File
 										}
 										}
 										else
 										else
 										{
 										{
-											$this->body = $decoder->data;
+											$this->body = trim($decoder->data);
 										}
 										}
 										break;
 										break;
 
 
@@ -298,7 +298,7 @@ class SimplePie_File
 		else
 		else
 		{
 		{
 			$this->method = SIMPLEPIE_FILE_SOURCE_LOCAL | SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS;
 			$this->method = SIMPLEPIE_FILE_SOURCE_LOCAL | SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS;
-			if (empty($url) || !($this->body = file_get_contents($url)))
+			if (empty($url) || !($this->body = trim(file_get_contents($url))))
 			{
 			{
 				$this->error = 'file_get_contents could not read the file';
 				$this->error = 'file_get_contents could not read the file';
 				$this->success = false;
 				$this->success = false;

+ 2 - 3
lib/SimplePie/SimplePie/HTTP/Parser.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 43 - 24
lib/SimplePie/SimplePie/IRI.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -259,6 +258,15 @@ class SimplePie_IRI
 		$this->set_iri($iri);
 		$this->set_iri($iri);
 	}
 	}
 
 
+	/**
+	 * Clean up
+	 */
+	public function __destruct() {
+	    $this->set_iri(null, true);
+	    $this->set_path(null, true);
+	    $this->set_authority(null, true);
+	}
+
 	/**
 	/**
 	 * Create a new IRI object by resolving a relative IRI
 	 * Create a new IRI object by resolving a relative IRI
 	 *
 	 *
@@ -768,24 +776,20 @@ class SimplePie_IRI
 	 */
 	 */
 	public function is_valid()
 	public function is_valid()
 	{
 	{
-		$isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null;
-		if ($this->ipath !== '' &&
-			(
-				$isauthority && (
-					$this->ipath[0] !== '/' ||
-					substr($this->ipath, 0, 2) === '//'
-				) ||
-				(
-					$this->scheme === null &&
-					!$isauthority &&
-					strpos($this->ipath, ':') !== false &&
-					(strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/'))
-				)
-			)
-		)
-		{
-			return false;
-		}
+		if ($this->ipath === '') return true;
+
+		$isauthority = $this->iuserinfo !== null || $this->ihost !== null ||
+			$this->port !== null;
+		if ($isauthority && $this->ipath[0] === '/') return true;
+
+		if (!$isauthority && (substr($this->ipath, 0, 2) === '//')) return false;
+
+		// Relative urls cannot have a colon in the first path segment (and the
+		// slashes themselves are not included so skip the first character).
+		if (!$this->scheme && !$isauthority &&
+		    strpos($this->ipath, ':') !== false &&
+		    strpos($this->ipath, '/', 1) !== false &&
+		    strpos($this->ipath, ':') < strpos($this->ipath, '/', 1)) return false;
 
 
 		return true;
 		return true;
 	}
 	}
@@ -797,9 +801,14 @@ class SimplePie_IRI
 	 * @param string $iri
 	 * @param string $iri
 	 * @return bool
 	 * @return bool
 	 */
 	 */
-	public function set_iri($iri)
+	public function set_iri($iri, $clear_cache = false)
 	{
 	{
 		static $cache;
 		static $cache;
+		if ($clear_cache) 
+		{
+			$cache = null;
+			return;
+		}
 		if (!$cache)
 		if (!$cache)
 		{
 		{
 			$cache = array();
 			$cache = array();
@@ -879,9 +888,14 @@ class SimplePie_IRI
 	 * @param string $authority
 	 * @param string $authority
 	 * @return bool
 	 * @return bool
 	 */
 	 */
-	public function set_authority($authority)
+	public function set_authority($authority, $clear_cache = false)
 	{
 	{
 		static $cache;
 		static $cache;
+		if ($clear_cache)
+		{
+			$cache = null;
+			return;
+		}
 		if (!$cache)
 		if (!$cache)
 			$cache = array();
 			$cache = array();
 
 
@@ -1049,9 +1063,14 @@ class SimplePie_IRI
 	 * @param string $ipath
 	 * @param string $ipath
 	 * @return bool
 	 * @return bool
 	 */
 	 */
-	public function set_path($ipath)
+	public function set_path($ipath, $clear_cache = false)
 	{
 	{
 		static $cache;
 		static $cache;
+		if ($clear_cache) 
+		{
+			$cache = null;
+			return;
+		}
 		if (!$cache)
 		if (!$cache)
 		{
 		{
 			$cache = array();
 			$cache = array();

+ 12 - 25
lib/SimplePie/SimplePie/Item.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -203,14 +202,13 @@ class SimplePie_Item
 	 *
 	 *
 	 * Uses `<atom:id>`, `<guid>`, `<dc:identifier>` or the `about` attribute
 	 * Uses `<atom:id>`, `<guid>`, `<dc:identifier>` or the `about` attribute
 	 * for RDF. If none of these are supplied (or `$hash` is true), creates an
 	 * for RDF. If none of these are supplied (or `$hash` is true), creates an
-	 * MD5 hash based on the permalink and title. If either of those are not
-	 * supplied, creates a hash based on the full feed data.
+	 * MD5 hash based on the permalink, title and content.
 	 *
 	 *
 	 * @since Beta 2
 	 * @since Beta 2
 	 * @param boolean $hash Should we force using a hash instead of the supplied ID?
 	 * @param boolean $hash Should we force using a hash instead of the supplied ID?
 	 * @return string
 	 * @return string
 	 */
 	 */
-	public function get_id($hash = false)
+	public function get_id($hash = false, $fn = '')
 	{
 	{
 		if (!$hash)
 		if (!$hash)
 		{
 		{
@@ -238,23 +236,10 @@ class SimplePie_Item
 			{
 			{
 				return $this->sanitize($this->data['attribs'][SIMPLEPIE_NAMESPACE_RDF]['about'], SIMPLEPIE_CONSTRUCT_TEXT);
 				return $this->sanitize($this->data['attribs'][SIMPLEPIE_NAMESPACE_RDF]['about'], SIMPLEPIE_CONSTRUCT_TEXT);
 			}
 			}
-			elseif (($return = $this->get_permalink()) !== null)
-			{
-				return $return;
-			}
-			elseif (($return = $this->get_title()) !== null)
-			{
-				return $return;
-			}
-		}
-		if ($this->get_permalink() !== null || $this->get_title() !== null)
-		{
-			return md5($this->get_permalink() . $this->get_title());
-		}
-		else
-		{
-			return md5(serialize($this->data));
 		}
 		}
+		if ($fn === '' || !is_callable($fn)) $fn = 'md5';
+		return call_user_func($fn,
+		       $this->get_permalink().$this->get_title().$this->get_content());
 	}
 	}
 
 
 	/**
 	/**
@@ -457,7 +442,7 @@ class SimplePie_Item
 	 * Uses `<atom:category>`, `<category>` or `<dc:subject>`
 	 * Uses `<atom:category>`, `<category>` or `<dc:subject>`
 	 *
 	 *
 	 * @since Beta 3
 	 * @since Beta 3
-	 * @return array|null List of {@see SimplePie_Category} objects
+	 * @return SimplePie_Category[]|null List of {@see SimplePie_Category} objects
 	 */
 	 */
 	public function get_categories()
 	public function get_categories()
 	{
 	{
@@ -1105,7 +1090,7 @@ class SimplePie_Item
 	 * @since Beta 2
 	 * @since Beta 2
 	 * @todo Add support for end-user defined sorting of enclosures by type/handler (so we can prefer the faster-loading FLV over MP4).
 	 * @todo Add support for end-user defined sorting of enclosures by type/handler (so we can prefer the faster-loading FLV over MP4).
 	 * @todo If an element exists at a level, but its value is empty, we should fall back to the value from the parent (if it exists).
 	 * @todo If an element exists at a level, but its value is empty, we should fall back to the value from the parent (if it exists).
-	 * @return array|null List of SimplePie_Enclosure items
+	 * @return SimplePie_Enclosure[]|null List of SimplePie_Enclosure items
 	 */
 	 */
 	public function get_enclosures()
 	public function get_enclosures()
 	{
 	{
@@ -2682,7 +2667,9 @@ class SimplePie_Item
 						// PLAYER
 						// PLAYER
 						if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player']))
 						if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player']))
 						{
 						{
-							$player = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI);
+							if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'])) {
+								$player = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI);
+							}
 						}
 						}
 						else
 						else
 						{
 						{

+ 55 - 5
lib/SimplePie/SimplePie/Locator.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -226,7 +225,7 @@ class SimplePie_Locator
 			}
 			}
 			if ($link->hasAttribute('href') && $link->hasAttribute('rel'))
 			if ($link->hasAttribute('href') && $link->hasAttribute('rel'))
 			{
 			{
-				$rel = array_unique($this->registry->call('Misc', 'space_seperated_tokens', array(strtolower($link->getAttribute('rel')))));
+				$rel = array_unique($this->registry->call('Misc', 'space_separated_tokens', array(strtolower($link->getAttribute('rel')))));
 				$line = method_exists($link, 'getLineNo') ? $link->getLineNo() : 1;
 				$line = method_exists($link, 'getLineNo') ? $link->getLineNo() : 1;
 
 
 				if ($this->base_location < $line)
 				if ($this->base_location < $line)
@@ -275,7 +274,7 @@ class SimplePie_Locator
 			{
 			{
 				$href = trim($link->getAttribute('href'));
 				$href = trim($link->getAttribute('href'));
 				$parsed = $this->registry->call('Misc', 'parse_url', array($href));
 				$parsed = $this->registry->call('Misc', 'parse_url', array($href));
-				if ($parsed['scheme'] === '' || preg_match('/^(http(s)|feed)?$/i', $parsed['scheme']))
+				if ($parsed['scheme'] === '' || preg_match('/^(https?|feed)?$/i', $parsed['scheme']))
 				{
 				{
 					if (method_exists($link, 'getLineNo') && $this->base_location < $link->getLineNo())
 					if (method_exists($link, 'getLineNo') && $this->base_location < $link->getLineNo())
 					{
 					{
@@ -312,6 +311,57 @@ class SimplePie_Locator
 		return null;
 		return null;
 	}
 	}
 
 
+	public function get_rel_link($rel)
+	{
+		if ($this->dom === null)
+		{
+			throw new SimplePie_Exception('DOMDocument not found, unable to use '.
+			                              'locator');
+		}
+		if (!class_exists('DOMXpath'))
+		{
+			throw new SimplePie_Exception('DOMXpath not found, unable to use '.
+			                              'get_rel_link');
+		}
+
+		$xpath = new DOMXpath($this->dom);
+		$query = '//a[@rel and @href] | //link[@rel and @href]';
+		foreach ($xpath->query($query) as $link)
+		{
+			$href = trim($link->getAttribute('href'));
+			$parsed = $this->registry->call('Misc', 'parse_url', array($href));
+			if ($parsed['scheme'] === '' ||
+			    preg_match('/^https?$/i', $parsed['scheme']))
+			{
+				if (method_exists($link, 'getLineNo') &&
+				    $this->base_location < $link->getLineNo())
+				{
+					$href =
+						$this->registry->call('Misc', 'absolutize_url',
+						                      array(trim($link->getAttribute('href')),
+						                            $this->base));
+				}
+				else
+				{
+					$href =
+						$this->registry->call('Misc', 'absolutize_url',
+						                      array(trim($link->getAttribute('href')),
+						                            $this->http_base));
+				}
+				if ($href === false)
+				{
+					return null;
+				}
+				$rel_values = explode(' ', strtolower($link->getAttribute('rel')));
+				if (in_array($rel, $rel_values))
+				{
+					return $href;
+				}
+			}
+		}
+		return null;
+	}
+
 	public function extension(&$array)
 	public function extension(&$array)
 	{
 	{
 		foreach ($array as $key => $value)
 		foreach ($array as $key => $value)

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

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -1947,7 +1946,7 @@ class SimplePie_Misc
 		return (bool) preg_match('/^([A-Za-z0-9\-._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!$&\'()*+,;=@]|(%[0-9ABCDEF]{2}))+$/u', $string);
 		return (bool) preg_match('/^([A-Za-z0-9\-._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!$&\'()*+,;=@]|(%[0-9ABCDEF]{2}))+$/u', $string);
 	}
 	}
 
 
-	public static function space_seperated_tokens($string)
+	public static function space_separated_tokens($string)
 	{
 	{
 		$space_characters = "\x20\x09\x0A\x0B\x0C\x0D";
 		$space_characters = "\x20\x09\x0A\x0B\x0C\x0D";
 		$string_length = strlen($string);
 		$string_length = strlen($string);
@@ -2178,7 +2177,8 @@ function embed_wmedia(width, height, link) {
 	/**
 	/**
 	 * Get the SimplePie build timestamp
 	 * Get the SimplePie build timestamp
 	 *
 	 *
-	 * Return SimplePie.php modification time.
+	 * Uses the git index if it exists, otherwise uses the modification time
+	 * of the newest file.
 	 */
 	 */
 	public static function get_build()
 	public static function get_build()
 	{
 	{

+ 2 - 3
lib/SimplePie/SimplePie/Net/IPv6.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 5 - 6
lib/SimplePie/SimplePie/Parse/Date.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -331,8 +330,8 @@ class SimplePie_Parse_Date
 		'CCT' => 23400,
 		'CCT' => 23400,
 		'CDT' => -18000,
 		'CDT' => -18000,
 		'CEDT' => 7200,
 		'CEDT' => 7200,
-		'CET' => 3600,
 		'CEST' => 7200,
 		'CEST' => 7200,
+		'CET' => 3600,
 		'CGST' => -7200,
 		'CGST' => -7200,
 		'CGT' => -10800,
 		'CGT' => -10800,
 		'CHADT' => 49500,
 		'CHADT' => 49500,
@@ -631,7 +630,7 @@ class SimplePie_Parse_Date
 	/**
 	/**
 	 * Parse a superset of W3C-DTF (allows hyphens and colons to be omitted, as
 	 * Parse a superset of W3C-DTF (allows hyphens and colons to be omitted, as
 	 * well as allowing any of upper or lower case "T", horizontal tabs, or
 	 * well as allowing any of upper or lower case "T", horizontal tabs, or
-	 * spaces to be used as the time seperator (including more than one))
+	 * spaces to be used as the time separator (including more than one))
 	 *
 	 *
 	 * @access protected
 	 * @access protected
 	 * @return int Timestamp
 	 * @return int Timestamp
@@ -691,7 +690,7 @@ class SimplePie_Parse_Date
 			}
 			}
 
 
 			// Convert the number of seconds to an integer, taking decimals into account
 			// Convert the number of seconds to an integer, taking decimals into account
-			$second = round($match[6] + $match[7] / pow(10, strlen($match[7])));
+			$second = round((int)$match[6] + (int)$match[7] / pow(10, strlen($match[7])));
 
 
 			return gmmktime($match[4], $match[5], $second, $match[2], $match[3], $match[1]) - $timezone;
 			return gmmktime($match[4], $match[5], $second, $match[2], $match[3], $match[1]) - $timezone;
 		}
 		}

+ 2 - 3
lib/SimplePie/SimplePie/Parser.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Rating.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

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

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/Restriction.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

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

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue
@@ -348,6 +347,7 @@ class SimplePie_Sanitize
 				}
 				}
 				$document = new DOMDocument();
 				$document = new DOMDocument();
 				$document->encoding = 'UTF-8';
 				$document->encoding = 'UTF-8';
+
 				$data = $this->preprocess($data, $type);
 				$data = $this->preprocess($data, $type);
 
 
 				set_error_handler(array('SimplePie_Misc', 'silence_errors'));
 				set_error_handler(array('SimplePie_Misc', 'silence_errors'));

+ 2 - 3
lib/SimplePie/SimplePie/Source.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/XML/Declaration/Parser.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 2 - 3
lib/SimplePie/SimplePie/gzdecode.php

@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
  *
- * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,8 +33,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  * POSSIBILITY OF SUCH DAMAGE.
  *
  *
  * @package SimplePie
  * @package SimplePie
- * @version 1.4-dev
- * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  * @author Ryan Parman
  * @author Ryan Parman
  * @author Geoffrey Sneddon
  * @author Geoffrey Sneddon
  * @author Ryan McCue
  * @author Ryan McCue

+ 98 - 32
lib/favicons.php

@@ -1,43 +1,109 @@
 <?php
 <?php
-
-include(LIB_PATH . '/Favicon/Favicon.php');
-include(LIB_PATH . '/Favicon/DataAccess.php');
-
 $favicons_dir = DATA_PATH . '/favicons/';
 $favicons_dir = DATA_PATH . '/favicons/';
 $default_favicon = PUBLIC_PATH . '/themes/icons/default_favicon.ico';
 $default_favicon = PUBLIC_PATH . '/themes/icons/default_favicon.ico';
 
 
-function download_favicon($website, $dest) {
-	global $favicons_dir, $default_favicon;
-
-	syslog(LOG_INFO, 'FreshRSS Favicon discovery GET ' . $website);
-	$favicon_getter = new \Favicon\Favicon();
-	$favicon_getter->setCacheDir($favicons_dir);
-	$favicon_url = $favicon_getter->get($website);
-
-	if ($favicon_url === false) {
-		return @copy($default_favicon, $dest);
+function isImgMime($content) {
+	//Based on https://github.com/ArthurHoaro/favicon/blob/3a4f93da9bb24915b21771eb7873a21bde26f5d1/src/Favicon/Favicon.php#L311-L319
+	if ($content == '') {
+		return false;
 	}
 	}
+	if (!extension_loaded('fileinfo')) {
+		return true;
+	}
+	$isImage = true;
+	try {
+		$fInfo = finfo_open(FILEINFO_MIME_TYPE);
+		$isImage = strpos(finfo_buffer($fInfo, $content), 'image') !== false;
+		finfo_close($fInfo);
+	} catch (Exception $e) {
+	}
+	return $isImage;
+}
 
 
-	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);
+function downloadHttp(&$url, $curlOptions = array()) {
+	syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url);
+	if (substr($url, 0, 2) === '//') {
+		$url = 'https:' . $favicon;
+	}
+	if ($url == '' || filter_var($url, FILTER_VALIDATE_URL) === false) {
+		return '';
+	}
+	$ch = curl_init($url);
+	curl_setopt_array($ch, array(
+			CURLOPT_FOLLOWLOCATION => true,
+			CURLOPT_MAXREDIRS => 10,
+			CURLOPT_RETURNTRANSFER => true,
+			CURLOPT_TIMEOUT => 15,
+			CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
+		));
+	if (defined('CURLOPT_ENCODING')) {
+		curl_setopt($ch, CURLOPT_ENCODING, '');	//Enable all encodings
+	}
+	curl_setopt_array($ch, $curlOptions);
+	$response = curl_exec($ch);
+	$info = curl_getinfo($ch);
+	curl_close($ch);
+	if (!empty($info['url']) && (filter_var($info['url'], FILTER_VALIDATE_URL) !== false)) {
+		$url = $info['url'];	//Possible redirect
+	}
+	return $info['http_code'] == 200 ? $response : '';
+}
 
 
-	if ($status_code === 200) {
-		$file = fopen($dest, 'w');
-		if ($file !== false) {
-			fwrite($file, $img_raw);
-			fclose($file);
-			return true;
+function searchFavicon(&$url) {
+	$dom = new DOMDocument();
+	$html = downloadHttp($url);
+	if ($html != '' && @$dom->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) {
+		$rels = array('shortcut icon', 'icon');
+		$links = $dom->getElementsByTagName('link');
+		foreach ($rels as $rel) {
+			foreach ($links as $link) {
+				if ($link->hasAttribute('rel') && $link->hasAttribute('href') &&
+					strtolower(trim($link->getAttribute('rel'))) === $rel) {
+					$href = trim($link->getAttribute('href'));
+					if (substr($href, 0, 2) === '//') {
+						// Case of protocol-relative URLs
+						if (preg_match('%^(https?:)//%i', $url, $matches)) {
+							$href = $matches[1] . $href;
+						} else {
+							$href = 'https:' . $href;
+						}
+					}
+					if (filter_var($href, FILTER_VALIDATE_URL) === false) {
+						$href = SimplePie_IRI::absolutize($url, $href);
+					}
+					$favicon = downloadHttp($href, array(
+							CURLOPT_REFERER => $url,
+						));
+					if (isImgMime($favicon)) {
+						return $favicon;
+					}
+				}
+			}
 		}
 		}
-	} else {
-		syslog(LOG_WARNING, 'FreshRSS Favicon GET ' . $favicon_url . ' error ' . $status_code);
 	}
 	}
+	return '';
+}
 
 
-	return false;
+function download_favicon($url, $dest) {
+	global $default_favicon;
+	$url = trim($url);
+	$favicon = searchFavicon($url);
+	if ($favicon == '') {
+		$rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url);
+		if ($rootUrl != $url) {
+			$url = $rootUrl;
+			$favicon = searchFavicon($url);
+		}
+		if ($favicon == '') {
+			$link = $rootUrl . 'favicon.ico';
+			$favicon = downloadHttp($link, array(
+					CURLOPT_REFERER => $url,
+				));
+			if (!isImgMime($favicon)) {
+				$favicon = '';
+			}
+		}
+	}
+	return ($favicon != '' && file_put_contents($dest, $favicon)) ||
+		@copy($default_favicon, $dest);
 }
 }

+ 1 - 1
lib/lib_install.php

@@ -67,7 +67,7 @@ function checkRequirements($dbType = '') {
 		'favicons' => $favicons ? 'ok' : 'ko',
 		'favicons' => $favicons ? 'ok' : 'ko',
 		'http_referer' => $http_referer ? 'ok' : 'ko',
 		'http_referer' => $http_referer ? 'ok' : 'ko',
 		'message' => $message ?: 'ok',
 		'message' => $message ?: 'ok',
-		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $fileinfo && $dom && $xml &&
+		'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $xml &&
 		         $data && $cache && $users && $favicons && $http_referer && $message == '' ?
 		         $data && $cache && $users && $favicons && $http_referer && $message == '' ?
 		         'ok' : 'ko'
 		         'ok' : 'ko'
 	);
 	);

+ 3 - 0
lib/lib_rss.php

@@ -334,6 +334,9 @@ function max_registrations_reached() {
  * @return a Minz_Configuration object, null if the configuration cannot be loaded.
  * @return a Minz_Configuration object, null if the configuration cannot be loaded.
  */
  */
 function get_user_configuration($username) {
 function get_user_configuration($username) {
+	if (!FreshRSS_user_Controller::checkUsername($username)) {
+		return null;
+	}
 	$namespace = 'user_' . $username;
 	$namespace = 'user_' . $username;
 	try {
 	try {
 		Minz_Configuration::register($namespace,
 		Minz_Configuration::register($namespace,

+ 30 - 9
p/api/pshb.php

@@ -23,8 +23,13 @@ if (!ctype_xdigit($key)) {
 chdir(PSHB_PATH);
 chdir(PSHB_PATH);
 $canonical64 = @file_get_contents('keys/' . $key . '.txt');
 $canonical64 = @file_get_contents('keys/' . $key . '.txt');
 if ($canonical64 === false) {
 if ($canonical64 === false) {
+	if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'unsubscribe') {
+		logMe('Warning: Accept unknown unsubscribe');
+		header('Connection: close');
+		exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : '');
+	}
 	header('HTTP/1.1 404 Not Found');
 	header('HTTP/1.1 404 Not Found');
-	logMe('Error: Feed key not found!: ' . $key);
+	logMe('Warning: Feed key not found!: ' . $key);
 	die('Feed key not found!');
 	die('Feed key not found!');
 }
 }
 $canonical64 = trim($canonical64);
 $canonical64 = trim($canonical64);
@@ -36,7 +41,7 @@ if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) {
 $hubFile = @file_get_contents('feeds/' . $canonical64 . '/!hub.json');
 $hubFile = @file_get_contents('feeds/' . $canonical64 . '/!hub.json');
 if ($hubFile === false) {
 if ($hubFile === false) {
 	header('HTTP/1.1 404 Not Found');
 	header('HTTP/1.1 404 Not Found');
-	//@unlink('keys/' . $key . '.txt');
+	unlink('keys/' . $key . '.txt');
 	logMe('Error: Feed info not found!: ' . $canonical64);
 	logMe('Error: Feed info not found!: ' . $canonical64);
 	die('Feed info not found!');
 	die('Feed info not found!');
 }
 }
@@ -50,8 +55,19 @@ chdir('feeds/' . $canonical64);
 $users = glob('*.txt', GLOB_NOSORT);
 $users = glob('*.txt', GLOB_NOSORT);
 if (empty($users)) {
 if (empty($users)) {
 	header('HTTP/1.1 410 Gone');
 	header('HTTP/1.1 410 Gone');
-	logMe('Error: Nobody is subscribed to this feed anymore!: ' . $canonical64);
-	die('Nobody is subscribed to this feed anymore!');
+	$url = base64url_decode($canonical64);
+	logMe('Warning: Nobody subscribes to this feed anymore!: ' . $url);
+	unlink('../../keys/' . $key . '.txt');
+	Minz_Configuration::register('system',
+		DATA_PATH . '/config.php',
+		DATA_PATH . '/config.default.php');
+	FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+	$feed = new FreshRSS_Feed($url);
+	$feed->pubSubHubbubSubscribe(false);
+	unlink('!hub.json');
+	chdir('..');
+	recursive_unlink($canonical64);
+	die('Nobody subscribes to this feed anymore!');
 }
 }
 
 
 if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') {
 if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') {
@@ -108,7 +124,9 @@ $nb = 0;
 foreach ($users as $userFilename) {
 foreach ($users as $userFilename) {
 	$username = basename($userFilename, '.txt');
 	$username = basename($userFilename, '.txt');
 	if (!file_exists(USERS_PATH . '/' . $username . '/config.php')) {
 	if (!file_exists(USERS_PATH . '/' . $username . '/config.php')) {
-		break;
+		logMe('Warning: Removing broken user link: ' . $username . ' for ' . $self);
+		unlink($userFilename);
+		continue;
 	}
 	}
 
 
 	try {
 	try {
@@ -119,11 +137,14 @@ foreach ($users as $userFilename) {
 		new Minz_ModelPdo($username);	//TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init
 		new Minz_ModelPdo($username);	//TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init
 		FreshRSS_Context::init();
 		FreshRSS_Context::init();
 		list($updated_feeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie);
 		list($updated_feeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie);
-		if ($updated_feeds > 0) {
+		if ($updated_feeds > 0 || $feed != false) {
 			$nb++;
 			$nb++;
+		} else {
+			logMe('Warning: User ' . $username . ' does not subscribe anymore to ' . $self);
+			unlink($userFilename);
 		}
 		}
 	} catch (Exception $e) {
 	} catch (Exception $e) {
-		logMe('Error: ' . $e->getMessage());
+		logMe('Error: ' . $e->getMessage() . ' for user ' . $username . ' and feed ' . $self);
 	}
 	}
 }
 }
 
 
@@ -132,8 +153,8 @@ unset($simplePie);
 
 
 if ($nb === 0) {
 if ($nb === 0) {
 	header('HTTP/1.1 410 Gone');
 	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!');
+	logMe('Warning: Nobody subscribes to this feed anymore after all!: ' . $self);
+	die('Nobody subscribes to this feed anymore after all!');
 } elseif (!empty($hubJson['error'])) {
 } elseif (!empty($hubJson['error'])) {
 	$hubJson['error'] = false;
 	$hubJson['error'] = false;
 	file_put_contents('./!hub.json', json_encode($hubJson));
 	file_put_contents('./!hub.json', json_encode($hubJson));

+ 1 - 2
p/f.php

@@ -1,6 +1,6 @@
 <?php
 <?php
-
 require('../constants.php');
 require('../constants.php');
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
 require(LIB_PATH . '/favicons.php');
 require(LIB_PATH . '/favicons.php');
 require(LIB_PATH . '/http-conditional.php');
 require(LIB_PATH . '/http-conditional.php');
 
 
@@ -15,7 +15,6 @@ function show_default_favicon($cacheSeconds = 3600) {
 	}
 	}
 }
 }
 
 
-
 $id = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '0';
 $id = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '0';
 if (!ctype_xdigit($id)) {
 if (!ctype_xdigit($id)) {
 	$id = '0';
 	$id = '0';

+ 11 - 3
p/scripts/main.js

@@ -803,12 +803,12 @@ function updateFeed(feeds, feeds_count) {
 	if (!feed) {
 	if (!feed) {
 		return;
 		return;
 	}
 	}
-
 	$.ajax({
 	$.ajax({
 		type: 'POST',
 		type: 'POST',
 		url: feed.url,
 		url: feed.url,
 		data : {
 		data : {
 			_csrf: context.csrf,
 			_csrf: context.csrf,
+			noCommit: feeds.length > 0 ? 1 : 0,
 		},
 		},
 	}).always(function (data) {
 	}).always(function (data) {
 		feed_processed++;
 		feed_processed++;
@@ -830,7 +830,6 @@ function init_actualize() {
 		if (ajax_loading) {
 		if (ajax_loading) {
 			return false;
 			return false;
 		}
 		}
-
 		ajax_loading = true;
 		ajax_loading = true;
 
 
 		$.getJSON('./?c=javascript&a=actualize').done(function (data) {
 		$.getJSON('./?c=javascript&a=actualize').done(function (data) {
@@ -841,7 +840,16 @@ function init_actualize() {
 			}
 			}
 			if (data.feeds.length === 0) {
 			if (data.feeds.length === 0) {
 				openNotification(data.feedback_no_refresh, "good");
 				openNotification(data.feedback_no_refresh, "good");
-				ajax_loading = false;
+				$.ajax({	//Empty request to force refresh server database cache
+					type: 'POST',
+					url: './?c=feed&a=actualize&id=-1',
+					data : {
+						_csrf: context.csrf,
+						noCommit: 0,
+					},
+				}).always(function (data) {
+					ajax_loading = false;
+				});
 				return;
 				return;
 			}
 			}
 			//Progress bar
 			//Progress bar

+ 1 - 1
p/themes/base-theme/template.css

@@ -345,7 +345,7 @@ a.btn {
 /*=== Tree */
 /*=== Tree */
 .tree {
 .tree {
 	margin: 0;
 	margin: 0;
-	padding: 0;
+	padding: 0 0 15em 0;
 	list-style: none;
 	list-style: none;
 	text-align: left;
 	text-align: left;
 }
 }

+ 7 - 0
tests/README.md

@@ -0,0 +1,7 @@
+# FreshRSS tests
+
+```sh
+cd ./tests/
+wget https://phar.phpunit.de/phpunit.phar
+php phpunit.phar --bootstrap bootstrap.php
+```

+ 1 - 1
tests/app/Models/CategoryTest.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 
 
-class FreshRSS_CategoryTest extends \PHPUnit_Framework_TestCase {
+class FreshRSS_CategoryTest extends PHPUnit\Framework\TestCase {
 
 
 	public function test__construct_whenNoParameters_createsObjectWithDefaultValues() {
 	public function test__construct_whenNoParameters_createsObjectWithDefaultValues() {
 		$category = new FreshRSS_Category();
 		$category = new FreshRSS_Category();

+ 0 - 5
tests/app/Models/ContextTest.php

@@ -1,5 +0,0 @@
-<?php
-
-class ContextTest extends \PHPUnit_Framework_TestCase {
-
-}

+ 75 - 74
tests/app/Models/SearchTest.php

@@ -2,7 +2,7 @@
 
 
 require_once(LIB_PATH . '/lib_date.php');
 require_once(LIB_PATH . '/lib_date.php');
 
 
-class SearchTest extends \PHPUnit_Framework_TestCase {
+class SearchTest extends PHPUnit\Framework\TestCase {
 
 
 	/**
 	/**
 	 * @dataProvider provideEmptyInput
 	 * @dataProvider provideEmptyInput
@@ -50,22 +50,22 @@ class SearchTest extends \PHPUnit_Framework_TestCase {
 	 */
 	 */
 	public function provideIntitleSearch() {
 	public function provideIntitleSearch() {
 		return array(
 		return array(
-		    array('intitle:word1', 'word1', null),
-		    array('intitle:word1 word2', 'word1', array('word2')),
-		    array('intitle:"word1 word2"', 'word1 word2', null),
-		    array("intitle:'word1 word2'", 'word1 word2', null),
-		    array('word1 intitle:word2', 'word2', array('word1')),
-		    array('word1 intitle:word2 word3', 'word2', array('word1', 'word3')),
-		    array('word1 intitle:"word2 word3"', 'word2 word3', array('word1')),
-		    array("word1 intitle:'word2 word3'", 'word2 word3', array('word1')),
-		    array('intitle:word1 intitle:word2', 'word1', array('intitle:word2')),
-		    array('intitle: word1 word2', null, array('word1', 'word2')),
-		    array('intitle:123', '123', null),
-		    array('intitle:"word1 word2" word3"', 'word1 word2', array('word3"')),
-		    array("intitle:'word1 word2' word3'", 'word1 word2', array("word3'")),
-		    array('intitle:"word1 word2\' word3"', "word1 word2' word3", null),
-		    array("intitle:'word1 word2\" word3'", 'word1 word2" word3', null),
-		    array("intitle:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')),
+		    array('intitle:word1', array('word1'), null),
+		    array('intitle:word1 word2', array('word1'), array('word2')),
+		    array('intitle:"word1 word2"', array('word1 word2'), null),
+		    array("intitle:'word1 word2'", array('word1 word2'), null),
+		    array('word1 intitle:word2', array('word2'), array('word1')),
+		    array('word1 intitle:word2 word3', array('word2'), array('word1', 'word3')),
+		    array('word1 intitle:"word2 word3"', array('word2 word3'), array('word1')),
+		    array("word1 intitle:'word2 word3'", array('word2 word3'), array('word1')),
+		    array('intitle:word1 intitle:word2', array('word1', 'word2'), null),
+		    array('intitle: word1 word2', array(), array('word1', 'word2')),
+		    array('intitle:123', array('123'), null),
+		    array('intitle:"word1 word2" word3"', array('word1 word2'), array('word3"')),
+		    array("intitle:'word1 word2' word3'", array('word1 word2'), array("word3'")),
+		    array('intitle:"word1 word2\' word3"', array("word1 word2' word3"), null),
+		    array("intitle:'word1 word2\" word3'", array('word1 word2" word3'), null),
+		    array("intitle:word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')),
 		);
 		);
 	}
 	}
 
 
@@ -86,22 +86,22 @@ class SearchTest extends \PHPUnit_Framework_TestCase {
 	 */
 	 */
 	public function provideAuthorSearch() {
 	public function provideAuthorSearch() {
 		return array(
 		return array(
-		    array('author:word1', 'word1', null),
-		    array('author:word1 word2', 'word1', array('word2')),
-		    array('author:"word1 word2"', 'word1 word2', null),
-		    array("author:'word1 word2'", 'word1 word2', null),
-		    array('word1 author:word2', 'word2', array('word1')),
-		    array('word1 author:word2 word3', 'word2', array('word1', 'word3')),
-		    array('word1 author:"word2 word3"', 'word2 word3', array('word1')),
-		    array("word1 author:'word2 word3'", 'word2 word3', array('word1')),
-		    array('author:word1 author:word2', 'word1', array('author:word2')),
-		    array('author: word1 word2', null, array('word1', 'word2')),
-		    array('author:123', '123', null),
-		    array('author:"word1 word2" word3"', 'word1 word2', array('word3"')),
-		    array("author:'word1 word2' word3'", 'word1 word2', array("word3'")),
-		    array('author:"word1 word2\' word3"', "word1 word2' word3", null),
-		    array("author:'word1 word2\" word3'", 'word1 word2" word3', null),
-		    array("author:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')),
+		    array('author:word1', array('word1'), null),
+		    array('author:word1 word2', array('word1'), array('word2')),
+		    array('author:"word1 word2"', array('word1 word2'), null),
+		    array("author:'word1 word2'", array('word1 word2'), null),
+		    array('word1 author:word2', array('word2'), array('word1')),
+		    array('word1 author:word2 word3', array('word2'), array('word1', 'word3')),
+		    array('word1 author:"word2 word3"', array('word2 word3'), array('word1')),
+		    array("word1 author:'word2 word3'", array('word2 word3'), array('word1')),
+		    array('author:word1 author:word2', array('word1', 'word2'), null),
+		    array('author: word1 word2', array(), array('word1', 'word2')),
+		    array('author:123', array('123'), null),
+		    array('author:"word1 word2" word3"', array('word1 word2'), array('word3"')),
+		    array("author:'word1 word2' word3'", array('word1 word2'), array("word3'")),
+		    array('author:"word1 word2\' word3"', array("word1 word2' word3"), null),
+		    array("author:'word1 word2\" word3'", array('word1 word2" word3'), null),
+		    array("author:word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')),
 		);
 		);
 	}
 	}
 
 
@@ -122,12 +122,13 @@ class SearchTest extends \PHPUnit_Framework_TestCase {
 	 */
 	 */
 	public function provideInurlSearch() {
 	public function provideInurlSearch() {
 		return array(
 		return array(
-		    array('inurl:word1', 'word1', null),
-		    array('inurl: word1', null, array('word1')),
-		    array('inurl:123', '123', null),
-		    array('inurl:word1 word2', 'word1', array('word2')),
-		    array('inurl:"word1 word2"', '"word1', array('word2"')),
-		    array("inurl:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')),
+		    array('inurl:word1', array('word1'), null),
+		    array('inurl: word1', array(), array('word1')),
+		    array('inurl:123', array('123'), null),
+		    array('inurl:word1 word2', array('word1'), array('word2')),
+		    array('inurl:"word1 word2"', array('"word1'), array('word2"')),
+		    array('inurl:word1 word2 inurl:word3', array('word1', 'word3'), array('word2')),
+		    array("inurl:word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')),
 		);
 		);
 	}
 	}
 
 
@@ -151,9 +152,9 @@ class SearchTest extends \PHPUnit_Framework_TestCase {
 		    array('date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'),
 		    array('date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'),
 		    array('date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'),
 		    array('date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'),
 		    array('date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'),
 		    array('date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'),
-		    array('date:2007-03-01/2008-05-11', '1172725200', '1210564799'),
-		    array('date:2007-03-01/', '1172725200', ''),
-		    array('date:/2008-05-11', '', '1210564799'),
+		    array('date:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1),
+		    array('date:2007-03-01/', strtotime('2007-03-01'), ''),
+		    array('date:/2008-05-11', '', strtotime('2008-05-12') - 1),
 		);
 		);
 	}
 	}
 
 
@@ -177,9 +178,9 @@ class SearchTest extends \PHPUnit_Framework_TestCase {
 		    array('pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'),
 		    array('pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'),
 		    array('pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'),
 		    array('pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'),
 		    array('pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'),
 		    array('pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'),
-		    array('pubdate:2007-03-01/2008-05-11', '1172725200', '1210564799'),
-		    array('pubdate:2007-03-01/', '1172725200', ''),
-		    array('pubdate:/2008-05-11', '', '1210564799'),
+		    array('pubdate:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1),
+		    array('pubdate:2007-03-01/', strtotime('2007-03-01'), ''),
+		    array('pubdate:/2008-05-11', '', strtotime('2008-05-12') - 1),
 		);
 		);
 	}
 	}
 
 
@@ -201,7 +202,7 @@ class SearchTest extends \PHPUnit_Framework_TestCase {
 	public function provideTagsSearch() {
 	public function provideTagsSearch() {
 		return array(
 		return array(
 		    array('#word1', array('word1'), null),
 		    array('#word1', array('word1'), null),
-		    array('# word1', null, array('#', 'word1')),
+		    array('# word1', array(), array('#', 'word1')),
 		    array('#123', array('123'), null),
 		    array('#123', array('123'), null),
 		    array('#word1 word2', array('word1'), array('word2')),
 		    array('#word1 word2', array('word1'), array('word2')),
 		    array('#"word1 word2"', array('"word1'), array('word2"')),
 		    array('#"word1 word2"', array('"word1'), array('word2"')),
@@ -241,49 +242,49 @@ class SearchTest extends \PHPUnit_Framework_TestCase {
 		return array(
 		return array(
 		    array(
 		    array(
 			'author:word1 date:2007-03-01/2008-05-11 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 #word5',
 			'author:word1 date:2007-03-01/2008-05-11 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 #word5',
-			'word1',
-			'1172725200',
-			'1210564799',
-			'word2',
-			'word3',
-			'1172725200',
-			'1210564799',
+			array('word1'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
+			array('word2'),
+			array('word3'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
 			array('word4', 'word5'),
 			array('word4', 'word5'),
 			null,
 			null,
 		    ),
 		    ),
 		    array(
 		    array(
 			'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 date:2007-03-01/2008-05-11',
 			'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 date:2007-03-01/2008-05-11',
-			'word1',
-			'1172725200',
-			'1210564799',
-			'word2',
-			'word3',
-			'1172725200',
-			'1210564799',
+			array('word1'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
+			array('word2'),
+			array('word3'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
 			array('word4', 'word5'),
 			array('word4', 'word5'),
 			array('word6'),
 			array('word6'),
 		    ),
 		    ),
 		    array(
 		    array(
 			'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 word7 date:2007-03-01/2008-05-11',
 			'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 word7 date:2007-03-01/2008-05-11',
-			'word1',
-			'1172725200',
-			'1210564799',
-			'word2',
-			'word3',
-			'1172725200',
-			'1210564799',
+			array('word1'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
+			array('word2'),
+			array('word3'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
 			array('word4', 'word5'),
 			array('word4', 'word5'),
 			array('word6', 'word7'),
 			array('word6', 'word7'),
 		    ),
 		    ),
 		    array(
 		    array(
 			'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 "word7 word8" date:2007-03-01/2008-05-11',
 			'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 "word7 word8" date:2007-03-01/2008-05-11',
-			'word1',
-			'1172725200',
-			'1210564799',
-			'word2',
-			'word3',
-			'1172725200',
-			'1210564799',
+			array('word1'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
+			array('word2'),
+			array('word3'),
+			strtotime('2007-03-01'),
+			strtotime('2008-05-12') - 1,
 			array('word4', 'word5'),
 			array('word4', 'word5'),
 			array('word7 word8', 'word6'),
 			array('word7 word8', 'word6'),
 		    ),
 		    ),

+ 1 - 1
tests/app/Models/UserQueryTest.php

@@ -3,7 +3,7 @@
 /**
 /**
  * Description of UserQueryTest
  * Description of UserQueryTest
  */
  */
-class UserQueryTest extends \PHPUnit_Framework_TestCase {
+class UserQueryTest extends PHPUnit\Framework\TestCase {
 
 
 	public function test__construct_whenAllQuery_storesAllParameters() {
 	public function test__construct_whenAllQuery_storesAllParameters() {
 		$query = array('get' => 'a');
 		$query = array('get' => 'a');