浏览代码

Merge pull request #1921 from FreshRSS/dev

FreshRSS 1.11.1
Alexandre Alapetite 7 年之前
父节点
当前提交
3306a1679c

+ 15 - 0
CHANGELOG.md

@@ -1,5 +1,20 @@
 # FreshRSS changelog
 
+## 2018-06-16 FreshRSS 1.11.1
+
+* Features
+	* Better support of `media:` tags such as thumbnails and descriptions (e.g. for YouTube) [#944](https://github.com/FreshRSS/FreshRSS/issues/944)
+* Extensions
+	* New extension mechanism allowing changing HTTP headers and other SimplePie parameters [#1924](https://github.com/FreshRSS/FreshRSS/pull/1924)
+	* Built-in extension to fix Tumblr feeds from European Union due to GDPR [#1894](https://github.com/FreshRSS/FreshRSS/issues/1894)
+* Bug fixing
+	* Fix bug in case of bad i18n in extensions [#1797](https://github.com/FreshRSS/FreshRSS/issues/1797)
+	* Fix extension callback for updated articles and PubSubHubbub [#1926](https://github.com/FreshRSS/FreshRSS/issues/1926)
+	* Fix regression in fetching full articles content [#1917](https://github.com/FreshRSS/FreshRSS/issues/1917)
+	* Fix several bugs in the new Fever API [#1930](https://github.com/FreshRSS/FreshRSS/issues/1930)
+	* Updated sharing to Mastodon [#1904](https://github.com/FreshRSS/FreshRSS/issues/1904)
+
+
 ## 2018-06-03 FreshRSS 1.11.0
 
 * API

+ 6 - 2
app/Controllers/extensionController.php

@@ -140,7 +140,7 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
 
 			if ($res === true) {
 				$ext_list = $conf->extensions_enabled;
-				array_push_unique($ext_list, $ext_name);
+				$ext_list[$ext_name] = true;
 				$conf->extensions_enabled = $ext_list;
 				$conf->save();
 
@@ -196,7 +196,11 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
 
 			if ($res === true) {
 				$ext_list = $conf->extensions_enabled;
-				array_remove($ext_list, $ext_name);
+				$legacyKey = array_search($ext_name, $ext_list, true);
+				if ($legacyKey !== false) {	//Legacy format FreshRSS < 1.11.1
+					unset($ext_list[$legacyKey]);
+				}
+				$ext_list[$ext_name] = false;
 				$conf->extensions_enabled = $ext_list;
 				$conf->save();
 

+ 8 - 1
app/Controllers/feedController.php

@@ -351,13 +351,20 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 							//This entry already exists and is unchanged. TODO: Remove the test with the zero'ed hash in FreshRSS v1.3
 							$oldGuids[] = $entry->guid();
 						} else {	//This entry already exists but has been updated
-							//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->url() .
 								//', old hash ' . $existingHash . ', new hash ' . $entry->hash());
 							$mark_updated_article_unread = $feed->attributes('mark_updated_article_unread') !== null ? (
 									$feed->attributes('mark_updated_article_unread')
 								) : FreshRSS_Context::$user_conf->mark_updated_article_unread;
 							$needFeedCacheRefresh = $mark_updated_article_unread;
 							$entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null);	//Change is_read according to policy.
+
+							$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
+							if ($entry === null) {
+								// An extension has returned a null value, there is nothing to insert.
+								continue;
+							}
+
 							if (!$entryDAO->inTransaction()) {
 								$entryDAO->beginTransaction();
 							}

+ 89 - 12
app/Models/Entry.php

@@ -17,10 +17,11 @@ class FreshRSS_Entry extends Minz_Model {
 	private $hash = null;
 	private $is_read;	//Nullable boolean
 	private $is_favorite;
+	private $feedId;
 	private $feed;
 	private $tags;
 
-	public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '',
+	public function __construct($feedId = '', $guid = '', $title = '', $author = '', $content = '',
 	                            $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
 		$this->_title($title);
 		$this->_author($author);
@@ -29,7 +30,7 @@ class FreshRSS_Entry extends Minz_Model {
 		$this->_date($pubdate);
 		$this->_isRead($is_read);
 		$this->_isFavorite($is_favorite);
-		$this->_feed($feed);
+		$this->_feedId($feedId);
 		$this->_tags(preg_split('/[\s#]/', $tags));
 		$this->_guid($guid);
 	}
@@ -75,10 +76,13 @@ class FreshRSS_Entry extends Minz_Model {
 	}
 	public function feed($object = false) {
 		if ($object) {
-			$feedDAO = FreshRSS_Factory::createFeedDao();
-			return $feedDAO->searchById($this->feed);
-		} else {
+			if ($this->feed == null) {
+				$feedDAO = FreshRSS_Factory::createFeedDao();
+				$this->feed = $feedDAO->searchById($this->feedId);
+			}
 			return $this->feed;
+		} else {
+			return $this->feedId;
 		}
 	}
 	public function tags($inString = false) {
@@ -145,7 +149,14 @@ class FreshRSS_Entry extends Minz_Model {
 		$this->is_favorite = $value;
 	}
 	public function _feed($value) {
-		$this->feed = $value;
+		if ($value != null) {
+			$this->feed = $value;
+			$this->feedId = $this->feed->id();
+		}
+	}
+	private function _feedId($value) {
+		$this->feed = null;
+		$this->feedId = $value;
 	}
 	public function _tags($value) {
 		$this->hash = null;
@@ -179,12 +190,74 @@ class FreshRSS_Entry extends Minz_Model {
 		}
 	}
 
-	public function loadCompleteContent($pathEntries) {
+	private static function get_content_by_parsing($url, $path, $attributes = array()) {
+		require_once(LIB_PATH . '/lib_phpQuery.php');
+		$system_conf = Minz_Configuration::get('system');
+		$limits = $system_conf->limits;
+		$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
+
+		if ($system_conf->simplepie_syslog_enabled) {
+			syslog(LOG_INFO, 'FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
+		}
+
+		$ch = curl_init();
+		curl_setopt_array($ch, array(
+			CURLOPT_URL => $url,
+			CURLOPT_REFERER => SimplePie_Misc::url_remove_credentials($url),
+			CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
+			CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
+			CURLOPT_CONNECTTIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
+			CURLOPT_TIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
+			//CURLOPT_FAILONERROR => true;
+			CURLOPT_MAXREDIRS => 4,
+			CURLOPT_RETURNTRANSFER => true,
+		));
+		if (version_compare(PHP_VERSION, '5.6.0') >= 0 || ini_get('open_basedir') == '') {
+			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);	//Keep option separated for open_basedir PHP bug 65646
+		}
+		if (defined('CURLOPT_ENCODING')) {
+			curl_setopt($ch, CURLOPT_ENCODING, '');	//Enable all encodings
+		}
+		curl_setopt_array($ch, $system_conf->curl_options);
+		if (isset($attributes['ssl_verify'])) {
+			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);
+			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $attributes['ssl_verify'] ? true : false);
+		}
+		$html = curl_exec($ch);
+		$c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+		$c_error = curl_error($ch);
+		curl_close($ch);
+
+		if ($c_status != 200 || $c_error != '') {
+			Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
+		}
+
+		if ($html) {
+			$doc = phpQuery::newDocument($html);
+			$content = $doc->find($path);
+
+			foreach (pq('img[data-src]') as $img) {
+				$imgP = pq($img);
+				$dataSrc = $imgP->attr('data-src');
+				if (strlen($dataSrc) > 4) {
+					$imgP->attr('src', $dataSrc);
+					$imgP->removeAttr('data-src');
+				}
+			}
+
+			return trim(sanitizeHTML($content->__toString(), $url));
+		} else {
+			throw new Exception();
+		}
+	}
+
+	public function loadCompleteContent() {
 		// Gestion du contenu
 		// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
-		if ($pathEntries) {
+		$feed = $this->feed(true);
+		if ($feed != null && trim($feed->pathEntries()) != '') {
 			$entryDAO = FreshRSS_Factory::createEntryDao();
-			$entry = $entryDAO->searchByGuid($this->feed, $this->guid);
+			$entry = $entryDAO->searchByGuid($this->feedId, $this->guid);
 
 			if ($entry) {
 				// l'article existe déjà en BDD, en se contente de recharger ce contenu
@@ -192,10 +265,14 @@ class FreshRSS_Entry extends Minz_Model {
 			} else {
 				try {
 					// l'article n'est pas en BDD, on va le chercher sur le site
-					$this->content = get_content_by_parsing(
-						htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries,
-						$this->feed->attributes()
+					$fullContent = self::get_content_by_parsing(
+						htmlspecialchars_decode($this->link(), ENT_QUOTES),
+						$feed->pathEntries(),
+						$feed->attributes()
 					);
+					if ($fullContent != '') {
+						$this->content = $fullContent;
+					}
 				} catch (Exception $e) {
 					// rien à faire, on garde l'ancien contenu(requête a échoué)
 					Minz_Log::warning($e->getMessage());

+ 11 - 6
app/Models/EntryDAO.php

@@ -904,8 +904,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	}
 
 	public function countUnreadRead() {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0'
-			. ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0 AND is_read=0';
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE f.priority > 0'
+			. ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE f.priority > 0 AND e.is_read=0';
 		$stm = $this->bd->prepare($sql);
 		$stm->execute();
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
@@ -914,9 +914,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
 	}
 	public function count($minPriority = null) {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';
 		if ($minPriority !== null) {
-			$sql = ' WHERE priority > ' . intval($minPriority);
+			$sql .= ' INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
+			$sql .= ' WHERE f.priority > ' . intval($minPriority);
 		}
 		$stm = $this->bd->prepare($sql);
 		$stm->execute();
@@ -924,9 +925,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $res[0];
 	}
 	public function countNotRead($minPriority = null) {
-		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE is_read=0';
+		$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';
 		if ($minPriority !== null) {
-			$sql = ' AND priority > ' . intval($minPriority);
+			$sql .= ' INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id';
+		}
+		$sql .= ' WHERE e.is_read=0';
+		if ($minPriority !== null) {
+			$sql .= ' AND f.priority > ' . intval($minPriority);
 		}
 		$stm = $this->bd->prepare($sql);
 		$stm->execute();

+ 52 - 20
app/Models/Feed.php

@@ -286,6 +286,7 @@ class FreshRSS_Feed extends Minz_Model {
 				if (!$loadDetails) {	//Only activates auto-discovery when adding a new feed
 					$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
 				}
+				Minz_ExtensionManager::callHook('simplepie_before_init', $feed, $this);
 				$mtime = $feed->init();
 
 				if ((!$mtime) || $feed->error()) {
@@ -355,24 +356,52 @@ class FreshRSS_Feed extends Minz_Model {
 
 			$content = html_only_entity_decode($item->get_content());
 
-			$elinks = array();
-			foreach ($item->get_enclosures() as $enclosure) {
-				$elink = $enclosure->get_link();
-				if ($elink != '' && empty($elinks[$elink])) {
-					$elinks[$elink] = '1';
-					$mime = strtolower($enclosure->get_type());
-					if (strpos($mime, 'image/') === 0) {
-						$content .= '<p class="enclosure"><img src="' . $elink . '" alt="" /></p>';
-					} elseif (strpos($mime, 'audio/') === 0) {
-						$content .= '<p class="enclosure"><audio preload="none" src="' . $elink
-							. '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
-					} elseif (strpos($mime, 'video/') === 0) {
-						$content .= '<p class="enclosure"><video preload="none" src="' . $elink
-							. '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
-					} elseif (strpos($mime, 'application/') === 0 || strpos($mime, 'text/') === 0) {
-						$content .= '<p class="enclosure"><a download="" href="' . $elink . '">💾</a></p>';
-					} else {
-						unset($elinks[$elink]);
+			if ($item->get_enclosures() != null) {
+				$elinks = array();
+				foreach ($item->get_enclosures() as $enclosure) {
+					$elink = $enclosure->get_link();
+					if ($elink != '' && empty($elinks[$elink])) {
+						$content .= '<div class="enclosure">';
+
+						if ($enclosure->get_title() != '') {
+							$content .= '<p class="enclosure-title">' . $enclosure->get_title() . '</p>';
+						}
+
+						$enclosureContent = '';
+						$elinks[$elink] = true;
+						$mime = strtolower($enclosure->get_type());
+						$medium = strtolower($enclosure->get_medium());
+						if ($medium === 'image' || strpos($mime, 'image/') === 0) {
+							$enclosureContent .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" /></p>';
+						} elseif ($medium === 'audio' || strpos($mime, 'audio/') === 0) {
+							$enclosureContent .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
+								. '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
+						} elseif ($medium === 'video' || strpos($mime, 'video/') === 0) {
+							$enclosureContent .= '<p class="enclosure-content"><video preload="none" src="' . $elink
+								. '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
+						} elseif ($medium != '' || strpos($mime, 'application/') === 0 || strpos($mime, 'text/') === 0) {
+							$enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink . '">💾</a></p>';
+						} else {
+							unset($elinks[$elink]);
+						}
+
+						$thumbnailContent = '';
+						if ($enclosure->get_thumbnails() != null) {
+							foreach ($enclosure->get_thumbnails() as $thumbnail) {
+								if (empty($elinks[$thumbnail])) {
+									$elinks[$thumbnail] = true;
+									$thumbnailContent .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" /></p>';
+								}
+							}
+						}
+
+						$content .= $thumbnailContent;
+						$content .= $enclosureContent;
+
+						if ($enclosure->get_description() != '') {
+							$content .= '<pre class="enclosure-description">' . $enclosure->get_description() . '</pre>';
+						}
+						$content .= "</div>\n";
 					}
 				}
 			}
@@ -391,8 +420,11 @@ class FreshRSS_Feed extends Minz_Model {
 				$date ? $date : time()
 			);
 			$entry->_tags($tags);
-			// permet de récupérer le contenu des flux tronqués
-			$entry->loadCompleteContent($this->pathEntries());
+			$entry->_feed($this);
+			if ($this->pathEntries != '') {
+				// Optionally load full content for truncated feeds
+				$entry->loadCompleteContent();
+			}
 
 			$entries[] = $entry;
 			unset($item);

+ 3 - 4
app/shares.php

@@ -120,11 +120,10 @@ return array(
 		'method' => 'GET',
 	),
 	'mastodon' => array(
-		'url' => '~URL~/api/v1/statuses',
-		'transform' => array(),
+		'url' => '~URL~/share?title=~TITLE~&url=~LINK~',
+		'transform' => array('rawurlencode'),
 		'form' => 'advanced',
-		'method' => 'POST',
-		'field' => 'status',
+		'method' => 'GET',
 	),
 	'pocket' => array(
 		'url' => 'https://getpocket.com/save?url=~LINK~&amp;title=~TITLE~',

+ 3 - 1
config.default.php

@@ -145,7 +145,9 @@ return array(
 	),
 
 	# List of enabled FreshRSS extensions.
-	'extensions_enabled' => array(),
+	'extensions_enabled' => array(
+		'Tumblr-GDPR' => true,
+	),
 
 	# Disable self-update,
 	'disable_update' => false,

+ 1 - 1
constants.php

@@ -2,7 +2,7 @@
 //NB: Do not edit; use ./constants.local.php instead.
 
 //<Not customisable>
-define('FRESHRSS_VERSION', '1.11.0');
+define('FRESHRSS_VERSION', '1.11.1');
 define('FRESHRSS_WEBSITE', 'https://freshrss.org');
 define('FRESHRSS_WIKI', 'https://freshrss.github.io/FreshRSS/');
 

+ 1 - 0
docs/en/developers/03_Backend/05_Extensions.md

@@ -366,6 +366,7 @@ The following events are available:
 - `entry_before_insert` (`function($entry) -> Entry | null`) : will be executed when a feed is refreshed and new entries will be imported into the database. The new entry (instance of FreshRSS_Entry) will be passed as parameter. 
 - `feed_before_insert` (`function($feed) -> Feed | null`) : will be executed when a new feed is imported into the database. The new feed (instance of FreshRSS_Feed) will be passed as parameter. 
 - `post_update` (`function(none) -> none`) : **TODO** add documentation
+- `simplepie_before_init` (`function($simplePie, $feed) -> none`) : **TODO** add documentation
 
 ### Writing your own configure.phtml
 

+ 1 - 1
docs/en/users/06_Fever_API.md

@@ -43,7 +43,7 @@ Following features are implemented:
 - setting starred marker for item(s)
 - setting read marker for feed
 - setting read marker for category
-- supports FreshRSS extensions, which use th `entry_before_display` hook
+- supports FreshRSS extensions, which use the `entry_before_display` hook
 
 Following features are not supported:
 - **Hot Links** aka **hot** as there is nothing in FreshRSS yet that is similar or could be used to simulate it

+ 1 - 0
docs/fr/developers/03_Backend/05_Extensions.md

@@ -329,6 +329,7 @@ TODO :
 - `entry_before_insert` (`function($entry) -> Entry | null`)
 - `feed_before_insert` (`function($feed) -> Feed | null`)
 - `post_update` (`function(none) -> none`)
+- `simplepie_before_init` (`function($simplePie, $feed) -> none`)
 
 ### Écrire le fichier configure.phtml
 

+ 16 - 0
docs/fr/users/06_Mobile_access.md

@@ -41,6 +41,22 @@ Voir la [page sur l’API compatible Fever](06_Fever_API.md) pour une autre poss
 	* Mettre à jour et retourner à l’étape 3.
 
 
+# Tests sur mobile
+
+6. Vous pouvez maintenant tester sur une application mobile (News+, FeedMe, ou EasyRSS sur Android)
+	* en utilisant comme adresse https://rss.example.net/api/greader.php ou http://example.net/FreshRSS/p/api/greader.php selon la configuration de votre site Web.
+	* ⚠️ attention aux majuscules et aux espaces en tapant l’adresse avec le clavier du mobile ⚠️ 
+	* avec votre nom d’utilisateur et le mot de passe enregistré au point 2 (mot de passe API).
+
+
+# En cas de problèmes
+
+ * Vous pouvez voir les logs API dans `./FreshRSS/data/users/_/log_api.txt`
+ * Si vous avez une erreur 404 (fichier non trouvé) lors de l’étape de test, et que vous êtes sous Apache,
+ voir http://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes pour utiliser News+ 
+(facultatif pour EasyRSS et FeedMe qui devraient fonctionner dès lors que vous obtenez un PASS au test *Check partial server configuration*).
+
+
 # Clients compatibles
 
 Tout client supportant une API de type Google Reader. Sélection :

+ 4 - 0
extensions/Tumblr-GDPR/README.md

@@ -0,0 +1,4 @@
+# Tumblr-GDPR
+
+Needed for accessing [Tumblr](https://www.tumblr.com/) RSS feeds from the European Union:
+bypass the [GPDR](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation) check, implying consent.

+ 13 - 0
extensions/Tumblr-GDPR/extension.php

@@ -0,0 +1,13 @@
+<?php
+
+class TumblrGdprExtension extends Minz_Extension {
+	public function init() {
+		$this->registerHook('simplepie_before_init', array('TumblrGdprExtension', 'curlHook'));
+	}
+
+	public static function curlHook($simplePie, $feed) {
+		if (preg_match('#^https?://[a-zA-Z_0-9-]+.tumblr.com/#i', $feed->url())) {
+			$simplePie->set_useragent(FRESHRSS_USERAGENT . ' like Googlebot');
+		}
+	}
+}

+ 8 - 0
extensions/Tumblr-GDPR/metadata.json

@@ -0,0 +1,8 @@
+{
+	"name": "Tumblr-GDPR",
+	"author": "Alkarex",
+	"description": "Bypass Tumblr’ GPDR check (implying consent) for the European Union",
+	"version": 1.0,
+	"entrypoint": "TumblrGdpr",
+	"type": "system"
+}

+ 7 - 7
lib/Minz/Configuration.php

@@ -90,15 +90,15 @@ class Minz_Configuration {
 	private $configuration_setter = null;
 
 	public function removeExtension($ext_name) {
-		self::$extensions_enabled = array_diff(
-			self::$extensions_enabled,
-			array($ext_name)
-		);
+		unset(self::$extensions_enabled[$ext_name]);
+		$legacyKey = array_search($ext_name, self::$extensions_enabled, true);
+		if ($legacyKey !== false) {	//Legacy format FreshRSS < 1.11.1
+			unset(self::$extensions_enabled[$legacyKey]);
+		}
 	}
 	public function addExtension($ext_name) {
-		$found = array_search($ext_name, self::$extensions_enabled) !== false;
-		if (!$found) {
-			self::$extensions_enabled[] = $ext_name;
+		if (!isset(self::$extensions_enabled[$ext_name])) {
+			self::$extensions_enabled[$ext_name] = true;
 		}
 	}
 

+ 23 - 6
lib/Minz/ExtensionManager.php

@@ -35,6 +35,10 @@ class Minz_ExtensionManager {
 			'list' => array(),
 			'signature' => 'OneToOne',
 		),
+		'simplepie_before_init' => array(  // function($simplePie, $feed) -> none
+			'list' => array(),
+			'signature' => 'PassArguments',
+		),
 	);
 	private static $ext_to_hooks = array();
 
@@ -160,7 +164,8 @@ class Minz_ExtensionManager {
 		self::$ext_list[$name] = $ext;
 
 		if ($ext->getType() === 'system' &&
-				in_array($name, self::$ext_auto_enabled)) {
+				(!empty(self::$ext_auto_enabled[$name]) ||
+				in_array($name, self::$ext_auto_enabled, true))) {	//Legacy format < FreshRSS 1.11.1
 			self::enable($ext->getName());
 		}
 
@@ -189,8 +194,15 @@ class Minz_ExtensionManager {
 	 * @param string[] $ext_list the names of extensions we want to load.
 	 */
 	public static function enableByList($ext_list) {
-		foreach ($ext_list as $ext_name) {
-			self::enable($ext_name);
+		if (!is_array($ext_list)) {
+			return;
+		}
+		foreach ($ext_list as $ext_name => $ext_status) {
+			if (is_int($ext_name)) {	//Legacy format int=>name
+				self::enable($ext_status);
+			} elseif ($ext_status) {	//New format name=>Boolean
+				self::enable($ext_name);
+			}
 		}
 	}
 
@@ -255,10 +267,15 @@ class Minz_ExtensionManager {
 		}
 
 		$signature = self::$hook_list[$hook_name]['signature'];
-		$signature = 'self::call' . $signature;
 		$args = func_get_args();
-
-		return call_user_func_array($signature, $args);
+		if ($signature === 'PassArguments') {
+			array_shift($args);
+			foreach (self::$hook_list[$hook_name]['list'] as $function) {
+				call_user_func_array($function, $args);
+			}
+		} else {
+			return call_user_func_array('self::call' . $signature, $args);
+		}
 	}
 
 	/**

+ 14 - 12
lib/Minz/Translate.php

@@ -64,12 +64,16 @@ class Minz_Translate {
 		$list_langs = array();
 
 		foreach (self::$path_list as $path) {
-			$path_langs = array_values(array_diff(
-				scandir($path),
-				array('..', '.')
-			));
-
-			$list_langs = array_merge($list_langs, $path_langs);
+			$scan = scandir($path);
+			if (is_array($scan)) {
+				$path_langs = array_values(array_diff(
+					$scan,
+					array('..', '.')
+				));
+				if (is_array($path_langs)) {
+					$list_langs = array_merge($list_langs, $path_langs);
+				}
+			}
 		}
 
 		return array_unique($list_langs);
@@ -80,12 +84,10 @@ class Minz_Translate {
 	 * @param $path a path containing i18n directories (e.g. ./en/, ./fr/).
 	 */
 	public static function registerPath($path) {
-		if (in_array($path, self::$path_list)) {
-			return;
+		if (!in_array($path, self::$path_list) && is_dir($path)) {
+			self::$path_list[] = $path;
+			self::loadLang($path);
 		}
-
-		self::$path_list[] = $path;
-		self::loadLang($path);
 	}
 
 	/**
@@ -94,7 +96,7 @@ class Minz_Translate {
 	 */
 	private static function loadLang($path) {
 		$lang_path = $path . '/' . self::$lang_name;
-		if (!file_exists($lang_path) || is_null(self::$lang_name)) {
+		if (!file_exists($lang_path) || self::$lang_name == '') {
 			// The lang path does not exist, nothing more to do.
 			return;
 		}

+ 6 - 1
lib/SimplePie/SimplePie.php

@@ -1322,7 +1322,12 @@ class SimplePie
 
 	function cleanMd5($rss)
 	{
-		return md5(preg_replace(array('#<(lastBuildDate|pubDate|updated|feedDate|dc:date|slash:comments)>[^<]+</\\1>#', '#<!--.+?-->#s'), '', $rss));
+		return md5(preg_replace(array(
+			'#<(lastBuildDate|pubDate|updated|feedDate|dc:date|slash:comments)>[^<]+</\\1>#',
+			'#<(media:starRating|media:statistics) [^/<>]+/>#',
+			'#<!--.+?-->#s',
+			), '', $rss));
+		
 	}
 
 	/**

+ 0 - 86
lib/lib_rss.php

@@ -253,68 +253,6 @@ function sanitizeHTML($data, $base = '') {
 	return html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_HTML, $base));
 }
 
-/* permet de récupérer le contenu d'un article pour un flux qui n'est pas complet */
-function get_content_by_parsing($url, $path, $attributes = array()) {
-	require_once(LIB_PATH . '/lib_phpQuery.php');
-	$system_conf = Minz_Configuration::get('system');
-	$limits = $system_conf->limits;
-	$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
-
-	if ($system_conf->simplepie_syslog_enabled) {
-		syslog(LOG_INFO, 'FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
-	}
-
-	$ch = curl_init();
-	curl_setopt_array($ch, array(
-		CURLOPT_URL => $url,
-		CURLOPT_REFERER => SimplePie_Misc::url_remove_credentials($url),
-		CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
-		CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
-		CURLOPT_CONNECTTIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
-		CURLOPT_TIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
-		//CURLOPT_FAILONERROR => true;
-		CURLOPT_MAXREDIRS => 4,
-		CURLOPT_RETURNTRANSFER => true,
-	));
-	if (version_compare(PHP_VERSION, '5.6.0') >= 0 || ini_get('open_basedir') == '') {
-		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);	//Keep option separated for open_basedir PHP bug 65646
-	}
-	if (defined('CURLOPT_ENCODING')) {
-		curl_setopt($ch, CURLOPT_ENCODING, '');	//Enable all encodings
-	}
-	curl_setopt_array($ch, $system_conf->curl_options);
-	if (isset($attributes['ssl_verify'])) {
-		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);
-		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $attributes['ssl_verify'] ? true : false);
-	}
-	$html = curl_exec($ch);
-	$c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-	$c_error = curl_error($ch);
-	curl_close($ch);
-
-	if ($c_status != 200 || $c_error != '') {
-		Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
-	}
-
-	if ($html) {
-		$doc = phpQuery::newDocument($html);
-		$content = $doc->find($path);
-
-		foreach (pq('img[data-src]') as $img) {
-			$imgP = pq($img);
-			$dataSrc = $imgP->attr('data-src');
-			if (strlen($dataSrc) > 4) {
-				$imgP->attr('src', $dataSrc);
-				$imgP->removeAttr('data-src');
-			}
-		}
-
-		return sanitizeHTML($content->__toString(), $url);
-	} else {
-		throw new Exception();
-	}
-}
-
 /**
  * Add support of image lazy loading
  * Move content from src attribute to data-original
@@ -542,7 +480,6 @@ function recursive_unlink($dir) {
 	return rmdir($dir);
 }
 
-
 /**
  * Remove queries where $get is appearing.
  * @param $get the get attribute which should be removed.
@@ -559,29 +496,6 @@ function remove_query_by_get($get, $queries) {
 	return $final_queries;
 }
 
-
-/**
- * Add a value in an array and take care it is unique.
- * @param $array the array in which we add the value.
- * @param $value the value to add.
- */
-function array_push_unique(&$array, $value) {
-	$found = array_search($value, $array) !== false;
-	if (!$found) {
-		$array[] = $value;
-	}
-}
-
-
-/**
- * Remove a value from an array.
- * @param $array the array from wich value is removed.
- * @param $value the value to remove.
- */
-function array_remove(&$array, $value) {
-	$array = array_diff($array, array($value));
-}
-
 //RFC 4648
 function base64url_encode($data) {
 	return strtr(rtrim(base64_encode($data), '='), '+/', '-_');

+ 93 - 168
p/api/fever.php

@@ -30,30 +30,8 @@ Minz_Session::init('FreshRSS');
 // ================================================================================================
 
 
-class FeverAPI_EntryDAO extends FreshRSS_EntryDAO
+class FeverDAO extends Minz_ModelPdo
 {
-	/**
-	 * @return array
-	 */
-	public function countFever()
-	{
-		$values = array(
-			'total' => 0,
-			'min' => 0,
-			'max' => 0,
-		);
-		$sql = 'SELECT COUNT(id) as `total`, MIN(id) as `min`, MAX(id) as `max` FROM `' . $this->prefix . 'entry`';
-		$stm = $this->bd->prepare($sql);
-		$stm->execute();
-		$result = $stm->fetchAll(PDO::FETCH_ASSOC);
-
-		if (!empty($result[0])) {
-			$values = $result[0];
-		}
-
-		return $values;
-	}
-
 	/**
 	 * @param string $prefix
 	 * @param array $values
@@ -81,14 +59,15 @@ class FeverAPI_EntryDAO extends FreshRSS_EntryDAO
 	{
 		$values = array();
 		$order = '';
+		$entryDAO = FreshRSS_Factory::createEntryDao();
 
 		$sql = 'SELECT id, guid, title, author, '
-			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
+			. ($entryDAO->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
 			. ', link, date, is_read, is_favorite, id_feed, tags '
 			. 'FROM `' . $this->prefix . 'entry` WHERE';
 
 		if (!empty($entry_ids)) {
-			$bindEntryIds = $this->bindParamArray("id", $entry_ids, $values);
+			$bindEntryIds = $this->bindParamArray('id', $entry_ids, $values);
 			$sql .= " id IN($bindEntryIds)";
 		} else if (!empty($max_id)) {
 			$sql .= ' id < :id';
@@ -101,7 +80,7 @@ class FeverAPI_EntryDAO extends FreshRSS_EntryDAO
 		}
 
 		if (!empty($feed_ids)) {
-			$bindFeedIds = $this->bindParamArray("feed", $feed_ids, $values);
+			$bindFeedIds = $this->bindParamArray('feed', $feed_ids, $values);
 			$sql .= " AND id_feed IN($bindFeedIds)";
 		}
 
@@ -114,7 +93,7 @@ class FeverAPI_EntryDAO extends FreshRSS_EntryDAO
 
 		$entries = array();
 		foreach ($result as $dao) {
-			$entries[] = self::daoToEntry($dao);
+			$entries[] = FreshRSS_EntryDAO::daoToEntry($dao);
 		}
 
 		return $entries;
@@ -130,6 +109,9 @@ class FeverAPI
 	const STATUS_OK = 1;
 	const STATUS_ERR = 0;
 
+	private $entryDAO = null;
+	private $feedDAO = null;
+
 	/**
 	 * Authenticate the user
 	 *
@@ -150,6 +132,8 @@ class FeverAPI
 				$user_conf = get_user_configuration($username);
 				if ($user_conf != null && $feverKey === $user_conf->feverKey) {
 					FreshRSS_Context::$user_conf = $user_conf;
+					$this->entryDAO = FreshRSS_Factory::createEntryDao();
+					$this->feedDAO = FreshRSS_Factory::createFeedDao();
 					return true;
 				}
 				Minz_Log::error('Fever API: Reset API password for user: ' . $username, API_LOG);
@@ -175,30 +159,6 @@ class FeverAPI
 		return false;
 	}
 
-	/**
-	 * @return FreshRSS_FeedDAO
-	 */
-	protected function getDaoForFeeds()
-	{
-		return new FreshRSS_FeedDAO();
-	}
-
-	/**
-	 * @return FreshRSS_CategoryDAO
-	 */
-	protected function getDaoForCategories()
-	{
-		return new FreshRSS_CategoryDAO();
-	}
-
-	/**
-	 * @return FeverAPI_EntryDAO
-	 */
-	protected function getDaoForEntries()
-	{
-		return new FeverAPI_EntryDAO();
-	}
-
 	/**
 	 * This does all the processing, since the fever api does not have a specific variable that specifies the operation
 	 *
@@ -213,65 +173,65 @@ class FeverAPI
 			throw new Exception('No user given or user is not allowed to access API');
 		}
 
-		if (isset($_REQUEST["groups"])) {
-			$response_arr["groups"] = $this->getGroups();
-			$response_arr["feeds_groups"] = $this->getFeedsGroup();
+		if (isset($_REQUEST['groups'])) {
+			$response_arr['groups'] = $this->getGroups();
+			$response_arr['feeds_groups'] = $this->getFeedsGroup();
 		}
 
-		if (isset($_REQUEST["feeds"])) {
-			$response_arr["feeds"] = $this->getFeeds();
-			$response_arr["feeds_groups"] = $this->getFeedsGroup();
+		if (isset($_REQUEST['feeds'])) {
+			$response_arr['feeds'] = $this->getFeeds();
+			$response_arr['feeds_groups'] = $this->getFeedsGroup();
 		}
 
-		if (isset($_REQUEST["favicons"])) {
-			$response_arr["favicons"] = $this->getFavicons();
+		if (isset($_REQUEST['favicons'])) {
+			$response_arr['favicons'] = $this->getFavicons();
 		}
 
-		if (isset($_REQUEST["items"])) {
-			$response_arr["total_items"] = $this->getTotalItems();
-			$response_arr["items"] = $this->getItems();
+		if (isset($_REQUEST['items'])) {
+			$response_arr['total_items'] = $this->getTotalItems();
+			$response_arr['items'] = $this->getItems();
 		}
 
-		if (isset($_REQUEST["links"])) {
-			$response_arr["links"] = $this->getLinks();
+		if (isset($_REQUEST['links'])) {
+			$response_arr['links'] = $this->getLinks();
 		}
 
-		if (isset($_REQUEST["unread_item_ids"])) {
-			$response_arr["unread_item_ids"] = $this->getUnreadItemIds();
+		if (isset($_REQUEST['unread_item_ids'])) {
+			$response_arr['unread_item_ids'] = $this->getUnreadItemIds();
 		}
 
-		if (isset($_REQUEST["saved_item_ids"])) {
-			$response_arr["saved_item_ids"] = $this->getSavedItemIds();
+		if (isset($_REQUEST['saved_item_ids'])) {
+			$response_arr['saved_item_ids'] = $this->getSavedItemIds();
 		}
 
-		if (isset($_REQUEST["mark"], $_REQUEST["as"], $_REQUEST["id"]) && is_numeric($_REQUEST["id"])) {
-			$method_name = "set" . ucfirst($_REQUEST["mark"]) . "As" . ucfirst($_REQUEST["as"]);
+		if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && is_numeric($_REQUEST['id'])) {
+			$method_name = 'set' . ucfirst($_REQUEST['mark']) . 'As' . ucfirst($_REQUEST['as']);
 			$allowedMethods = array(
 				'setFeedAsRead', 'setGroupAsRead', 'setItemAsRead',
 				'setItemAsSaved', 'setItemAsUnread', 'setItemAsUnsaved'
 			);
 			if (in_array($method_name, $allowedMethods)) {
-				$id = intval($_REQUEST["id"]);
-				switch (strtolower($_REQUEST["mark"])) {
+				$id = intval($_REQUEST['id']);
+				switch (strtolower($_REQUEST['mark'])) {
 					case 'item':
 						$this->{$method_name}($id);
 						break;
 					case 'feed':
 					case 'group':
-						$before = (isset($_REQUEST["before"])) ? $_REQUEST["before"] : null;
+						$before = isset($_REQUEST['before']) ? $_REQUEST['before'] : null;
 						$this->{$method_name}($id, $before);
 						break;
 				}
 
-				switch ($_REQUEST["as"]) {
-					case "read":
-					case "unread":
-						$response_arr["unread_item_ids"] = $this->getUnreadItemIds();
+				switch ($_REQUEST['as']) {
+					case 'read':
+					case 'unread':
+						$response_arr['unread_item_ids'] = $this->getUnreadItemIds();
 						break;
 
 					case 'saved':
 					case 'unsaved':
-						$response_arr["saved_item_ids"] = $this->getSavedItemIds();
+						$response_arr['saved_item_ids'] = $this->getSavedItemIds();
 						break;
 				}
 			}
@@ -308,8 +268,7 @@ class FeverAPI
 	{
 		$lastUpdate = 0;
 
-		$dao = $this->getDaoForFeeds();
-		$entries = $dao->listFeedsOrderUpdate(-1, 1);
+		$entries = $this->feedDAO->listFeedsOrderUpdate(-1, 1);
 		$feed = current($entries);
 
 		if (!empty($feed)) {
@@ -325,20 +284,18 @@ class FeverAPI
 	protected function getFeeds()
 	{
 		$feeds = array();
-
-		$dao = $this->getDaoForFeeds();
-		$myFeeds = $dao->listFeeds();
+		$myFeeds = $this->feedDAO->listFeeds();
 
 		/** @var FreshRSS_Feed $feed */
 		foreach ($myFeeds as $feed) {
 			$feeds[] = array(
-				"id" => $feed->id(),
-				"favicon_id" => $feed->id(),
-				"title" => $feed->name(),
-				"url" => $feed->url(),
-				"site_url" => $feed->website(),
-				"is_spark" => 0, // unsupported
-				"last_updated_on_time" => $feed->lastUpdate()
+				'id' => $feed->id(),
+				'favicon_id' => $feed->id(),
+				'title' => $feed->name(),
+				'url' => $feed->url(),
+				'site_url' => $feed->website(),
+				'is_spark' => 0, // unsupported
+				'last_updated_on_time' => $feed->lastUpdate(),
 			);
 		}
 
@@ -352,14 +309,14 @@ class FeverAPI
 	{
 		$groups = array();
 
-		$dao = $this->getDaoForCategories();
-		$categories = $dao->listCategories(false, false);
+		$categoryDAO = new FreshRSS_CategoryDAO();
+		$categories = $categoryDAO->listCategories(false, false);
 
 		/** @var FreshRSS_Category $category */
 		foreach ($categories as $category) {
 			$groups[] = array(
 				'id' => $category->id(),
-				'title' => $category->name()
+				'title' => $category->name(),
 			);
 		}
 
@@ -372,11 +329,8 @@ class FeverAPI
 	protected function getFavicons()
 	{
 		$favicons = array();
-
-		$dao = $this->getDaoForFeeds();
-		$myFeeds = $dao->listFeeds();
-
 		$salt = FreshRSS_Context::$system_conf->salt;
+		$myFeeds = $this->feedDAO->listFeeds();
 
 		/** @var FreshRSS_Feed $feed */
 		foreach ($myFeeds as $feed) {
@@ -388,8 +342,8 @@ class FeverAPI
 			}
 
 			$favicons[] = array(
-				"id" => $feed->id(),
-				"data" => image_type_to_mime_type(exif_imagetype($filename)) . ";base64," . base64_encode(file_get_contents($filename))
+				'id' => $feed->id(),
+				'data' => image_type_to_mime_type(exif_imagetype($filename)) . ';base64,' . base64_encode(file_get_contents($filename))
 			);
 		}
 
@@ -401,16 +355,7 @@ class FeverAPI
 	 */
 	protected function getTotalItems()
 	{
-		$total_items = 0;
-
-		$dao = $this->getDaoForEntries();
-		$result = $dao->countFever();
-
-		if (!empty($result)) {
-			$total_items = $result['total'];
-		}
-
-		return $total_items;
+		return $this->entryDAO->count();
 	}
 
 	/**
@@ -420,9 +365,7 @@ class FeverAPI
 	{
 		$groups = array();
 		$ids = array();
-
-		$dao = $this->getDaoForFeeds();
-		$myFeeds = $dao->listFeeds();
+		$myFeeds = $this->feedDAO->listFeeds();
 
 		/** @var FreshRSS_Feed $feed */
 		foreach ($myFeeds as $feed) {
@@ -462,8 +405,7 @@ class FeverAPI
 	 */
 	protected function getUnreadItemIds()
 	{
-		$dao = $this->getDaoForEntries();
-		$entries = $dao->listIdsWhere('a', '', FreshRSS_Entry::STATE_NOT_READ, 'ASC', 0);
+		$entries = $this->entryDAO->listIdsWhere('a', '', FreshRSS_Entry::STATE_NOT_READ, 'ASC', 0);
 		return $this->entriesToIdList($entries);
 	}
 
@@ -472,33 +414,28 @@ class FeverAPI
 	 */
 	protected function getSavedItemIds()
 	{
-		$dao = $this->getDaoForEntries();
-		$entries = $dao->listIdsWhere('a', '', FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0);
+		$entries = $this->entryDAO->listIdsWhere('a', '', FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0);
 		return $this->entriesToIdList($entries);
 	}
 
 	protected function setItemAsRead($id)
 	{
-		$dao = $this->getDaoForEntries();
-		$dao->markRead($id, true);
+		return $this->entryDAO->markRead($id, true);
 	}
 
 	protected function setItemAsUnread($id)
 	{
-		$dao = $this->getDaoForEntries();
-		$dao->markRead($id, false);
+		return $this->entryDAO->markRead($id, false);
 	}
 
 	protected function setItemAsSaved($id)
 	{
-		$dao = $this->getDaoForEntries();
-		$dao->markFavorite($id, true);
+		return $this->entryDAO->markFavorite($id, true);
 	}
 
 	protected function setItemAsUnsaved($id)
 	{
-		$dao = $this->getDaoForEntries();
-		$dao->markFavorite($id, false);
+		return $this->entryDAO->markFavorite($id, false);
 	}
 
 	/**
@@ -511,17 +448,17 @@ class FeverAPI
 		$max_id = null;
 		$since_id = null;
 
-		if (isset($_REQUEST["feed_ids"]) || isset($_REQUEST["group_ids"])) {
-			if (isset($_REQUEST["feed_ids"])) {
-				$feed_ids = explode(",", $_REQUEST["feed_ids"]);
+		if (isset($_REQUEST['feed_ids']) || isset($_REQUEST['group_ids'])) {
+			if (isset($_REQUEST['feed_ids'])) {
+				$feed_ids = explode(',', $_REQUEST['feed_ids']);
 			}
 
-			$dao = $this->getDaoForCategories();
-			if (isset($_REQUEST["group_ids"])) {
-				$group_ids = explode(",", $_REQUEST["group_ids"]);
+			if (isset($_REQUEST['group_ids'])) {
+				$categoryDAO = new FreshRSS_CategoryDAO();
+				$group_ids = explode(',', $_REQUEST['group_ids']);
 				foreach ($group_ids as $id) {
 					/** @var FreshRSS_Category $category */
-					$category = $dao->searchById($id);
+					$category = $categoryDAO->searchById($id);	//TODO: Transform to SQL query without loop! Consider FreshRSS_CategoryDAO::listCategories(true)
 					/** @var FreshRSS_Feed $feed */
 					foreach ($category->feeds() as $feed) {
 						$feeds[] = $feed->id();
@@ -532,25 +469,25 @@ class FeverAPI
 			}
 		}
 
-		if (isset($_REQUEST["max_id"])) {
+		if (isset($_REQUEST['max_id'])) {
 			// use the max_id argument to request the previous $item_limit items
-			if (is_numeric($_REQUEST["max_id"])) {
-				$max = ($_REQUEST["max_id"] > 0) ? intval($_REQUEST["max_id"]) : 0;
+			if (is_numeric($_REQUEST['max_id'])) {
+				$max = $_REQUEST['max_id'] > 0 ? intval($_REQUEST['max_id']) : 0;
 				if ($max) {
 					$max_id = $max;
 				}
 			}
-		} else if (isset($_REQUEST["with_ids"])) {
-			$entry_ids = explode(",", $_REQUEST["with_ids"]);
+		} else if (isset($_REQUEST['with_ids'])) {
+			$entry_ids = explode(',', $_REQUEST['with_ids']);
 		} else {
 			// use the since_id argument to request the next $item_limit items
-			$since_id = isset($_REQUEST["since_id"]) && is_numeric($_REQUEST["since_id"]) ? intval($_REQUEST["since_id"]) : 0;
+			$since_id = isset($_REQUEST['since_id']) && is_numeric($_REQUEST['since_id']) ? intval($_REQUEST['since_id']) : 0;
 		}
 
 		$items = array();
 
-		$dao = $this->getDaoForEntries();
-		$entries = $dao->findEntries($feed_ids, $entry_ids, $max_id, $since_id);
+		$feverDAO = new FeverDAO();
+		$entries = $feverDAO->findEntries($feed_ids, $entry_ids, $max_id, $since_id);
 
 		// Load list of extensions and enable the "system" ones.
 		Minz_ExtensionManager::init();
@@ -562,15 +499,15 @@ class FeverAPI
 				continue;
 			}
 			$items[] = array(
-				"id" => $entry->id(),
-				"feed_id" => $entry->feed(false),
-				"title" => $entry->title(),
-				"author" => $entry->author(),
-				"html" => $entry->content(),
-				"url" => $entry->link(),
-				"is_saved" => $entry->isFavorite() ? 1 : 0,
-				"is_read" => $entry->isRead() ? 1 : 0,
-				"created_on_time" => $entry->date(true)
+				'id' => $entry->id(),
+				'feed_id' => $entry->feed(false),
+				'title' => $entry->title(),
+				'author' => $entry->author(),
+				'html' => $entry->content(),
+				'url' => $entry->link(),
+				'is_saved' => $entry->isFavorite() ? 1 : 0,
+				'is_read' => $entry->isRead() ? 1 : 0,
+				'created_on_time' => $entry->date(true),
 			);
 		}
 
@@ -585,43 +522,31 @@ class FeverAPI
 	 */
 	protected function convertBeforeToId($beforeTimestamp)
 	{
-		// if before is zero, set it to now so feeds all items are read from before this point in time
-		if ($beforeTimestamp == 0) {
-			$before = time();
-		}
-		$before = PHP_INT_MAX;
-
-		return $before;
+		return $beforeTimestamp == 0 ? 0 : $beforeTimestamp . '000000';
 	}
 
 	protected function setFeedAsRead($id, $before)
 	{
 		$before = $this->convertBeforeToId($before);
-		$dao = $this->getDaoForEntries();
-		return $dao->markReadFeed($id, $before);
+		return $this->entryDAO->markReadFeed($id, $before);
 	}
 
 	protected function setGroupAsRead($id, $before)
 	{
-		$dao = $this->getDaoForEntries();
+		$before = $this->convertBeforeToId($before);
 
 		// special case to mark all items as read
-		if ($id === 0) {
-			$result = $dao->countFever();
-
-			if (!empty($result)) {
-				return $dao->markReadEntries($result['max']);
-			}
+		if ($id == 0) {
+			return $this->entryDAO->markReadEntries($before);
 		}
 
-		$before = $this->convertBeforeToId($before);
-		return $dao->markReadCat($id, $before);
+		return $this->entryDAO->markReadCat($id, $before);
 	}
 }
 
 // ================================================================================================
 // refresh is not allowed yet, probably we find a way to support it later
-if (isset($_REQUEST["refresh"])) {
+if (isset($_REQUEST['refresh'])) {
 	Minz_Log::warning('Fever API: Refresh items - notImplemented()', API_LOG);
 	header('HTTP/1.1 501 Not Implemented');
 	header('Content-Type: text/plain; charset=UTF-8');
@@ -631,7 +556,7 @@ if (isset($_REQUEST["refresh"])) {
 // Start the Fever API handling
 $handler = new FeverAPI();
 
-header("Content-Type: application/json; charset=UTF-8");
+header('Content-Type: application/json; charset=UTF-8');
 
 if (!$handler->isAuthenticatedApiUser()) {
 	echo $handler->wrap(FeverAPI::STATUS_ERR, array());

+ 6 - 0
p/api/pshb.php

@@ -116,6 +116,8 @@ if ($self !== base64url_decode($canonical64)) {
 	$self = base64url_decode($canonical64);
 }
 
+Minz_ExtensionManager::init();
+
 $nb = 0;
 foreach ($users as $userFilename) {
 	$username = basename($userFilename, '.txt');
@@ -132,6 +134,10 @@ foreach ($users as $userFilename) {
 		                             join_path(FRESHRSS_PATH, 'config-user.default.php'));
 		new Minz_ModelPdo($username);	//TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init
 		FreshRSS_Context::init();
+		if (FreshRSS_Context::$user_conf != null) {
+			Minz_ExtensionManager::enableByList(FreshRSS_Context::$user_conf->extensions_enabled);
+		}
+
 		list($updated_feeds, $feed, $nb_new_articles) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie);
 		if ($updated_feeds > 0 || $feed != false) {
 			$nb++;

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

@@ -832,11 +832,15 @@ input:checked + .slide-container .properties {
 	display: none;
 }
 
-.enclosure > [download] {
+.enclosure [download] {
 	font-size: xx-large;
 	margin-left: .8em;
 }
 
+pre.enclosure-description {
+	white-space: pre-line;
+}
+
 /*=== MOBILE */
 /*===========*/
 @media(max-width: 840px) {