Merge pull request #1921 from FreshRSS/dev

FreshRSS 1.11.1
pull/1975/head 1.11.1
Alexandre Alapetite 6 years ago committed by GitHub
commit 3306a1679c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      CHANGELOG.md
  2. 8
      app/Controllers/extensionController.php
  3. 9
      app/Controllers/feedController.php
  4. 97
      app/Models/Entry.php
  5. 17
      app/Models/EntryDAO.php
  6. 54
      app/Models/Feed.php
  7. 7
      app/shares.php
  8. 4
      config.default.php
  9. 2
      constants.php
  10. 1
      docs/en/developers/03_Backend/05_Extensions.md
  11. 2
      docs/en/users/06_Fever_API.md
  12. 1
      docs/fr/developers/03_Backend/05_Extensions.md
  13. 16
      docs/fr/users/06_Mobile_access.md
  14. 4
      extensions/Tumblr-GDPR/README.md
  15. 13
      extensions/Tumblr-GDPR/extension.php
  16. 8
      extensions/Tumblr-GDPR/metadata.json
  17. 14
      lib/Minz/Configuration.php
  18. 27
      lib/Minz/ExtensionManager.php
  19. 16
      lib/Minz/Translate.php
  20. 7
      lib/SimplePie/SimplePie.php
  21. 86
      lib/lib_rss.php
  22. 261
      p/api/fever.php
  23. 6
      p/api/pshb.php
  24. 6
      p/themes/base-theme/template.css

@ -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

@ -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();

@ -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();
}

@ -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) {
if ($this->feed == null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
return $feedDAO->searchById($this->feed);
} else {
$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) {
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());

@ -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();

@ -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,25 +356,53 @@ class FreshRSS_Feed extends Minz_Model {
$content = html_only_entity_decode($item->get_content());
if ($item->get_enclosures() != null) {
$elinks = array();
foreach ($item->get_enclosures() as $enclosure) {
$elink = $enclosure->get_link();
if ($elink != '' && empty($elinks[$elink])) {
$elinks[$elink] = '1';
$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());
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
$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 (strpos($mime, 'video/') === 0) {
$content .= '<p class="enclosure"><video preload="none" src="' . $elink
} 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 (strpos($mime, 'application/') === 0 || strpos($mime, 'text/') === 0) {
$content .= '<p class="enclosure"><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);

@ -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~',

@ -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,

@ -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/');

@ -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

@ -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

@ -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

@ -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 :

@ -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.

@ -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');
}
}
}

@ -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"
}

@ -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;
}
}

@ -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,10 +194,17 @@ 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) {
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);
}
}
}
/**
* Return a list of extensions.
@ -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);
}
}
/**

@ -64,13 +64,17 @@ class Minz_Translate {
$list_langs = array();
foreach (self::$path_list as $path) {
$scan = scandir($path);
if (is_array($scan)) {
$path_langs = array_values(array_diff(
scandir($path),
$scan,
array('..', '.')
));
if (is_array($path_langs)) {
$list_langs = array_merge($list_langs, $path_langs);
}
}
}
return array_unique($list_langs);
}
@ -80,13 +84,11 @@ 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);
}
}
/**
* Load translations of the current language from the given 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;
}

@ -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));
}
/**

@ -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), '='), '+/', '-_');

@ -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());

@ -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++;

@ -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) {

Loading…
Cancel
Save