From eea69848661c6385d6c29c84c98a2315072c5e8e Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 11 Mar 2018 11:53:28 +0100 Subject: SQLite more error logs https://github.com/FreshRSS/FreshRSS/issues/1816 And reformat SQL, which required vertical scrolling for me :-) --- app/Models/FeedDAO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/Models/FeedDAO.php') diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 0c25ab0ba..5c6e613d3 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -105,7 +105,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::error('SQL error updateFeed: ' . $info[2]); + Minz_Log::error('SQL error updateFeed: ' . $info[2] . ' for feed ' . $id); return false; } } -- cgit v1.2.3 From b552abb3327f09baa1c0f4e821dc9f6bd6ef738e Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 1 May 2018 17:02:11 +0200 Subject: JSON column for feeds (#1838) * Draft of JSON column for feeds https://github.com/FreshRSS/FreshRSS/issues/1654 * Add some per-feed options * Feed cURL timeout * Mark updated articles as read https://github.com/FreshRSS/FreshRSS/issues/891 * Mark as read upon reception https://github.com/FreshRSS/FreshRSS/issues/1702 * Ignore SSL (unsafe) https://github.com/FreshRSS/FreshRSS/issues/1811 * Try PHPCS workaround While waiting for a better syntax support --- app/Controllers/feedController.php | 15 ++++-- app/Controllers/subscriptionController.php | 19 ++++++-- app/Models/DatabaseDAO.php | 2 +- app/Models/Factory.php | 8 +++- app/Models/Feed.php | 26 ++++++++++- app/Models/FeedDAO.php | 73 ++++++++++++++++++++++++++---- app/Models/FeedDAOSQLite.php | 17 +++++++ app/SQL/install.sql.mysql.php | 1 + app/SQL/install.sql.pgsql.php | 1 + app/SQL/install.sql.sqlite.php | 1 + app/i18n/cz/sub.php | 2 + app/i18n/de/sub.php | 2 + app/i18n/en/sub.php | 2 + app/i18n/es/sub.php | 2 + app/i18n/fr/sub.php | 2 + app/i18n/he/sub.php | 2 + app/i18n/it/sub.php | 2 + app/i18n/kr/sub.php | 2 + app/i18n/nl/sub.php | 2 + app/i18n/pt-br/sub.php | 2 + app/i18n/ru/sub.php | 2 + app/i18n/tr/sub.php | 2 + app/i18n/zh-cn/sub.php | 2 + app/views/helpers/feed/update.phtml | 47 +++++++++++++++++++ lib/Minz/ModelPdo.php | 4 +- lib/Minz/Request.php | 13 ++++++ lib/lib_rss.php | 15 ++++-- 27 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 app/Models/FeedDAOSQLite.php (limited to 'app/Models/FeedDAO.php') diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 59c22b777..ca85e7cb8 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -84,6 +84,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { 'description' => $feed->description(), 'lastUpdate' => time(), 'httpAuth' => $feed->httpAuth(), + 'attributes' => array(), ); $id = $feedDAO->addFeed($values); @@ -271,7 +272,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $updated_feeds = 0; $nb_new_articles = 0; - $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { $url = $feed->url(); //For detection of HTTP 301 @@ -353,8 +353,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } else { //This entry already exists but has been updated //Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() . //', old hash ' . $existingHash . ', new hash ' . $entry->hash()); - //TODO: Make an updated/is_read policy by feed, in addition to the global one. - $needFeedCacheRefresh = FreshRSS_Context::$user_conf->mark_updated_article_unread; + $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. if (!$entryDAO->inTransaction()) { $entryDAO->beginTransaction(); @@ -365,15 +367,18 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // This entry should not be added considering configuration and date. $oldGuids[] = $entry->guid(); } else { + $read_upon_reception = $feed->attributes('read_upon_reception') !== null ? ( + $feed->attributes('read_upon_reception') + ) : FreshRSS_Context::$user_conf->mark_when['reception']; if ($isNewFeed) { $id = min(time(), $entry_date) . uSecString(); - $entry->_isRead($is_read); + $entry->_isRead($read_upon_reception); } elseif ($entry_date < $date_min) { $id = min(time(), $entry_date) . uSecString(); $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read } else { $id = uTimeString(); - $entry->_isRead($is_read); + $entry->_isRead($read_upon_reception); } $entry->_id($id); diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index 37efd3b57..860cd912f 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -15,7 +15,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { } $catDAO = new FreshRSS_CategoryDAO(); - $feedDAO = new FreshRSS_FeedDAO(); + $feedDAO = FreshRSS_Factory::createFeedDao(); $catDAO->checkDefault(); $feedDAO->updateTTL(); @@ -74,9 +74,10 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { return; } - $this->view->feed = $this->view->feeds[$id]; + $feed = $this->view->feeds[$id]; + $this->view->feed = $feed; - Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · '); + Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $feed->name() . ' · '); if (Minz_Request::isPost()) { $user = trim(Minz_Request::param('http_user_feed' . $id, '')); @@ -95,6 +96,13 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $ttl = FreshRSS_Context::$user_conf->ttl_default; } + $feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread')); + $feed->_attributes('read_upon_reception', Minz_Request::paramTernary('read_upon_reception')); + $feed->_attributes('ssl_verify', Minz_Request::paramTernary('ssl_verify')); + + $timeout = intval(Minz_Request::param('timeout', 0)); + $feed->_attributes('timeout', $timeout > 0 ? $timeout : null); + $values = array( 'name' => Minz_Request::param('name', ''), 'description' => sanitizeHTML(Minz_Request::param('description', '', true)), @@ -106,14 +114,15 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { 'httpAuth' => $httpAuth, 'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)), 'ttl' => $ttl * ($mute ? -1 : 1), + 'attributes' => $feed->attributes() ); invalidateHttpCache(); $url_redirect = array('c' => 'subscription', 'params' => array('id' => $id)); if ($feedDAO->updateFeed($id, $values) !== false) { - $this->view->feed->_category($cat); - $this->view->feed->faviconPrepare(); + $feed->_category($cat); + $feed->faviconPrepare(); Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect); } else { diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php index f5469f2b7..b8e5577e4 100644 --- a/app/Models/DatabaseDAO.php +++ b/app/Models/DatabaseDAO.php @@ -50,7 +50,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { public function feedIsCorrect() { return $this->checkTable('feed', array( 'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate', - 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', + 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes', 'cache_nbEntries', 'cache_nbUnreads' )); } diff --git a/app/Models/Factory.php b/app/Models/Factory.php index dfccc883e..764987c46 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -3,7 +3,13 @@ class FreshRSS_Factory { public static function createFeedDao($username = null) { - return new FreshRSS_FeedDAO($username); + $conf = Minz_Configuration::get('system'); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_FeedDAOSQLite($username); + default: + return new FreshRSS_FeedDAO($username); + } } public static function createEntryDao($username = null) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 196d94931..04101c10d 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -26,6 +26,7 @@ class FreshRSS_Feed extends Minz_Model { private $error = false; private $keep_history = self::KEEP_HISTORY_DEFAULT; private $ttl = self::TTL_DEFAULT; + private $attributes = array(); private $mute = false; private $hash = null; private $lockPath = ''; @@ -114,6 +115,13 @@ class FreshRSS_Feed extends Minz_Model { public function ttl() { return $this->ttl; } + public function attributes($key = '') { + if ($key == '') { + return $this->attributes; + } else { + return isset($this->attributes[$key]) ? $this->attributes[$key] : null; + } + } public function mute() { return $this->mute; } @@ -234,6 +242,22 @@ class FreshRSS_Feed extends Minz_Model { $this->ttl = abs($value); $this->mute = $value < self::TTL_DEFAULT; } + + public function _attributes($key, $value) { + if ($key == '') { + if (is_string($value)) { + $value = json_decode($value, true); + } + if (is_array($value)) { + $this->attributes = $value; + } + } elseif ($value === null) { + unset($this->attributes[$key]); + } else { + $this->attributes[$key] = $value; + } + } + public function _nbNotRead($value) { $this->nbNotRead = intval($value); } @@ -253,7 +277,7 @@ class FreshRSS_Feed extends Minz_Model { if ($this->httpAuth != '') { $url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url); } - $feed = customSimplePie(); + $feed = customSimplePie($this->attributes()); if (substr($url, -11) === '#force_feed') { $feed->force_feed(true); $url = substr($url, 0, -11); diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 5c6e613d3..f968ae98b 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -1,6 +1,33 @@ bd->prepare('ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN attributes TEXT'); + return $stm && $stm->execute(); + } + } catch (Exception $e) { + Minz_Log::error('FreshRSS_FeedDAO::addColumn error: ' . $e->getMessage()); + } + return false; + } + + protected function autoUpdateDb($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] === '42S22' || $errorInfo[0] === '42703') { //ER_BAD_FIELD_ERROR (Mysql), undefined_column (PostgreSQL) + foreach (array('attributes') as $column) { + if (stripos($errorInfo[2], $column) !== false) { + return $this->addColumn($column); + } + } + } + } + return false; + } + public function addFeed($valuesTmp) { $sql = ' INSERT INTO `' . $this->prefix . 'feed` @@ -15,10 +42,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { `httpAuth`, error, keep_history, - ttl + ttl, + attributes ) VALUES - (?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?)'; + (?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?, ?)'; $stm = $this->bd->prepare($sql); $valuesTmp['url'] = safe_ascii($valuesTmp['url']); @@ -34,12 +62,16 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { base64_encode($valuesTmp['httpAuth']), FreshRSS_Feed::KEEP_HISTORY_DEFAULT, FreshRSS_Feed::TTL_DEFAULT, + isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '', ); if ($stm && $stm->execute($values)) { return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"'); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->addFeed($valuesTmp); + } Minz_Log::error('SQL error addFeed: ' . $info[2]); return false; } @@ -60,7 +92,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { 'website' => $feed->website(), 'description' => $feed->description(), 'lastUpdate' => 0, - 'httpAuth' => $feed->httpAuth() + 'httpAuth' => $feed->httpAuth(), + 'attributes' => $feed->attributes(), ); $id = $this->addFeed($values); @@ -87,8 +120,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { foreach ($valuesTmp as $key => $v) { $set .= '`' . $key . '`=?, '; - if ($key == 'httpAuth') { + if ($key === 'httpAuth') { $valuesTmp[$key] = base64_encode($v); + } elseif ($key === 'attributes') { + $valuesTmp[$key] = json_encode($v); } } $set = substr($set, 0, -2); @@ -105,11 +140,25 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $stm->rowCount(); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->updateFeed($id, $valuesTmp); + } Minz_Log::error('SQL error updateFeed: ' . $info[2] . ' for feed ' . $id); return false; } } + public function updateFeedAttribute($feed, $key, $value) { + if ($feed instanceof FreshRSS_Feed) { + $feed->_attributes($key, $value); + return $this->updateFeed( + $feed->id(), + array('attributes' => $feed->attributes()) + ); + } + return false; + } + public function updateLastUpdate($id, $inError = false, $mtime = 0) { //See also updateCachedValue() $sql = 'UPDATE `' . $this->prefix . 'feed` ' . 'SET `lastUpdate`=?, error=? ' @@ -252,15 +301,22 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { */ public function listFeedsOrderUpdate($defaultCacheDuration = 3600) { $this->updateTTL(); - $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl ' + $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes ' . 'FROM `' . $this->prefix . 'feed` ' . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT . ' AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ') . 'ORDER BY `lastUpdate`'; $stm = $this->bd->prepare($sql); - $stm->execute(); - - return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); + if ($stm && $stm->execute()) { + return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); + } else { + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->listFeedsOrderUpdate($defaultCacheDuration); + } + Minz_Log::error('SQL error listFeedsOrderUpdate: ' . $info[2]); + return array(); + } } public function listByCategory($cat) { @@ -385,6 +441,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $myFeed->_error(isset($dao['error']) ? $dao['error'] : 0); $myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : FreshRSS_Feed::KEEP_HISTORY_DEFAULT); $myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT); + $myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : ''); $myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0); $myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0); if (isset($dao['id'])) { diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php new file mode 100644 index 000000000..3c203b378 --- /dev/null +++ b/app/Models/FeedDAOSQLite.php @@ -0,0 +1,17 @@ +bd->query("PRAGMA table_info('feed')")) { + $columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1); + foreach (array('attributes') as $column) { + if (!in_array($column, $columns)) { + return $this->addColumn($column); + } + } + } + return false; + } + +} diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index b94a24298..747a0a6b3 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` ( `error` boolean DEFAULT 0, `keep_history` MEDIUMINT NOT NULL DEFAULT -2, -- v0.7 `ttl` INT NOT NULL DEFAULT 0, -- v0.7.3 + `attributes` TEXT, -- v1.11.0 `cache_nbEntries` int DEFAULT 0, -- v0.7 `cache_nbUnreads` int DEFAULT 0, -- v0.7 PRIMARY KEY (`id`), diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php index 23afdb783..99f5a05d3 100644 --- a/app/SQL/install.sql.pgsql.php +++ b/app/SQL/install.sql.pgsql.php @@ -22,6 +22,7 @@ $SQL_CREATE_TABLES = array( "error" smallint DEFAULT 0, "keep_history" INT NOT NULL DEFAULT -2, "ttl" INT NOT NULL DEFAULT 0, + "attributes" TEXT, -- v1.11.0 "cache_nbEntries" INT DEFAULT 0, "cache_nbUnreads" INT DEFAULT 0, FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index d8e670bc8..cbfb719e5 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -21,6 +21,7 @@ $SQL_CREATE_TABLES = array( `error` boolean DEFAULT 0, `keep_history` MEDIUMINT NOT NULL DEFAULT -2, `ttl` INT NOT NULL DEFAULT 0, + `attributes` TEXT, -- v1.11.0 `cache_nbEntries` int DEFAULT 0, `cache_nbUnreads` int DEFAULT 0, FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index ec77be317..5caf9acbe 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'Zobrazit ve “Všechny kanály”', 'normal' => 'Show in its category', // TODO ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'Statistika', 'think_to_add' => 'Můžete přidat kanály.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Název', 'title_add' => 'Přidat RSS kanál', 'ttl' => 'Neobnovovat častěji než', diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 7f74c275e..0ba818c69 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'In Haupt-Feeds zeigen', 'normal' => 'Zeige in eigener Kategorie', ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'Statistiken', 'think_to_add' => 'Sie können Feeds hinzufügen.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Titel', 'title_add' => 'Einen RSS-Feed hinzufügen', 'ttl' => 'Aktualisiere automatisch nicht öfter als', diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index b9bae7955..5ff41a4b3 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'Show in main stream', 'normal' => 'Show in its category', ), + 'ssl_verify' => 'Verify SSL security', 'stats' => 'Statistics', 'think_to_add' => 'You may add some feeds.', + 'timeout' => 'Timeout in seconds', 'title' => 'Title', 'title_add' => 'Add a RSS feed', 'ttl' => 'Do not automatically refresh more often than', diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php index 091c1e3e3..3abc85578 100755 --- a/app/i18n/es/sub.php +++ b/app/i18n/es/sub.php @@ -39,8 +39,10 @@ return array( 'main_stream' => 'Mostrar en salida principal', 'normal' => 'Show in its category', // TODO ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'Estadísticas', 'think_to_add' => 'Puedes añadir fuentes.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Título', 'title_add' => 'Añadir fuente RSS', 'ttl' => 'No actualizar de forma automática con una frecuencia mayor a', diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index 04be55aa5..c6af2fb90 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'Afficher dans le flux principal', 'normal' => 'Afficher dans sa catégorie', ), + 'ssl_verify' => 'Vérification sécurité SSL', 'stats' => 'Statistiques', 'think_to_add' => 'Vous pouvez ajouter des flux.', + 'timeout' => 'Délai d’attente en secondes', 'title' => 'Titre', 'title_add' => 'Ajouter un flux RSS', 'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que', diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php index 849a1d5bd..a263cd728 100644 --- a/app/i18n/he/sub.php +++ b/app/i18n/he/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'הצגה בזרם המרכזי', 'normal' => 'Show in its category', // TODO ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'סטטיסטיקות', 'think_to_add' => 'ניתן להוסיף הזנות חדשות.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'כותרת', 'title_add' => 'הוספת הזנה', 'ttl' => 'אין לרענן אוטומטית יותר מ', diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php index 698e64481..22d58a27f 100644 --- a/app/i18n/it/sub.php +++ b/app/i18n/it/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'Mostra in homepage', // TODO 'normal' => 'Show in its category', // TODO ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'Statistiche', 'think_to_add' => 'Aggiungi feed.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Titolo', 'title_add' => 'Aggiungi RSS feed', 'ttl' => 'Non aggiornare automaticamente piu di', diff --git a/app/i18n/kr/sub.php b/app/i18n/kr/sub.php index e11d4588f..464b64f70 100644 --- a/app/i18n/kr/sub.php +++ b/app/i18n/kr/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => '메인 스트림에 표시하기', 'normal' => '피드가 속한 카테고리에만 표시하기', ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => '통계', 'think_to_add' => '피드를 추가할 수 있습니다.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => '제목', 'title_add' => 'RSS 피드 추가', 'ttl' => '다음 시간이 지나기 전에 새로고침 금지', diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index 6b1ac268b..067e226aa 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -45,8 +45,10 @@ return array( 'normal' => 'Show in its category', // TODO ), 'pubsubhubbub' => 'Directe notificaties met PubSubHubbub', + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'Statistieken', 'think_to_add' => 'Voeg wat feeds toe.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Titel', 'title_add' => 'Voeg een RSS feed toe', 'ttl' => 'Vernieuw automatisch niet vaker dan', diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php index 09dde718f..1b084f08f 100644 --- a/app/i18n/pt-br/sub.php +++ b/app/i18n/pt-br/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'Mostrar na tela principal', 'normal' => 'Show in its category', // TODO ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'Estatísticas', 'think_to_add' => 'Você deve adicionar alguns feeds.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Título', 'title_add' => 'Adicionar o RSS feed', 'ttl' => 'Não atualize automáticamente mais que', diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php index 9e360630a..bef49623f 100644 --- a/app/i18n/ru/sub.php +++ b/app/i18n/ru/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'Show in main stream', // TODO 'normal' => 'Show in its category', // TODO ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'Statistics',// TODO 'think_to_add' => 'You may add some feeds.',// TODO + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Title',// TODO 'title_add' => 'Add a RSS feed',// TODO 'ttl' => 'Do not automatically refresh more often than',// TODO diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php index 871731158..e8cd15d0d 100644 --- a/app/i18n/tr/sub.php +++ b/app/i18n/tr/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => 'Ana akışda göster', 'normal' => 'Show in its category', // TODO ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => 'İstatistikler', 'think_to_add' => 'Akış ekleyebilirsiniz.', + 'timeout' => 'Timeout in seconds', //TODO 'title' => 'Başlık', 'title_add' => 'RSS akışı ekle', 'ttl' => 'Şu kadar süreden fazla otomatik yenileme yapma', diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php index bf73f82c4..034f8a9d9 100644 --- a/app/i18n/zh-cn/sub.php +++ b/app/i18n/zh-cn/sub.php @@ -44,8 +44,10 @@ return array( 'main_stream' => '在首页中显示', 'normal' => '在分类中显示', ), + 'ssl_verify' => 'Verify SSL security', //TODO 'stats' => '统计', 'think_to_add' => '你可以添加一些 RSS 源。', + 'timeout' => 'Timeout in seconds', //TODO 'title' => '标题', 'title_add' => '添加 RSS 源', 'ttl' => '最小自动更新时间', diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index d379c5df8..01c90369c 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -178,6 +178,53 @@ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index d769e0ff4..6928a2857 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -69,7 +69,7 @@ class Minz_ModelPdo { case 'sqlite': $string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite'); $this->prefix = ''; - $this->bd = new MinzPDOMSQLite($string, $db['user'], $db['password'], $driver_options); + $this->bd = new MinzPDOSQLite($string, $db['user'], $db['password'], $driver_options); $this->bd->exec('PRAGMA foreign_keys = ON;'); break; case 'pgsql': @@ -160,7 +160,7 @@ class MinzPDOMySql extends MinzPDO { } } -class MinzPDOMSQLite extends MinzPDO { +class MinzPDOSQLite extends MinzPDO { public function lastInsertId($name = null) { return parent::lastInsertId(); //We discard the name, only used by PostgreSQL } diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index a43509ded..e21697e42 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -39,6 +39,19 @@ class Minz_Request { return $default; } } + public static function paramTernary($key) { + if (isset(self::$params[$key])) { + $p = self::$params[$key]; + $tp = trim($p); + if ($p === null || $tp === '' || $tp === 'null') { + return null; + } elseif ($p == false || $tp == '0' || $tp === 'false' || $tp === 'no') { + return false; + } + return true; + } + return null; + } public static function defaultControllerName() { return self::$default_controller_name; } diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 215c4c362..9dfca385d 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -175,7 +175,7 @@ function html_only_entity_decode($text) { return strtr($text, $htmlEntitiesOnly); } -function customSimplePie() { +function customSimplePie($attributes = array()) { $system_conf = Minz_Configuration::get('system'); $limits = $system_conf->limits; $simplePie = new SimplePie(); @@ -183,8 +183,17 @@ function customSimplePie() { $simplePie->set_syslog($system_conf->simplepie_syslog_enabled); $simplePie->set_cache_location(CACHE_PATH); $simplePie->set_cache_duration($limits['cache_duration']); - $simplePie->set_timeout($limits['timeout']); - $simplePie->set_curl_options($system_conf->curl_options); + + $feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']); + $simplePie->set_timeout($feed_timeout > 0 ? $feed_timeout : $limits['timeout']); + + $curl_options = $system_conf->curl_options; + if (isset($attributes['ssl_verify'])) { + $curl_options[CURLOPT_SSL_VERIFYHOST] = $attributes['ssl_verify'] ? 2 : 0; + $curl_options[CURLOPT_SSL_VERIFYPEER] = $attributes['ssl_verify'] ? true : false; + } + $simplePie->set_curl_options($curl_options); + $simplePie->strip_htmltags(array( 'base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', -- cgit v1.2.3 From 8f1bad60d0b7bd0d0a05bcdcf3c6834e39c0c6eb Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Thu, 24 May 2018 21:53:47 +0200 Subject: Add Fever API and user documentation (#1836) * added fever api and documentation * spaces to tabs * fixed code format * added links * added utf8 to header * removed XML support * removed before check, as we have to convert it afterwards * added sandboxed setting (currently disabled) added support for extensions using entry_before_display * listFeedsOrderUpdate LIMIT https://github.com/FreshRSS/FreshRSS/pull/1836/files#r175287881 * removed custom sql by using FreshRSS_FeedDAO::listFeedsOrderUpdate() * fixed mark all as read * replaced custom sql for getUnread() and getStarred() with dao functions * removed sanitization functions * Rework fever login * Fix config bug Plus documentation * Fix array syntax For compatibility with PHP 5.3 * Disable cookies and session for API * Fix currentUser * added response header and error log * adjusted phpdoc to match new authentication * Mechanism to delete old keys * replace PHP_INT_MAX with zero to disable limit * replace method_exists with check for explicit methods * removed Press support and smaller refactoring + updated docu * Rewrite bindParamArray Avoid one of the SQL injection risks * Docs and readme * Fix API link * Simplify reverse key check Using userConfig --- README.fr.md | 49 ++- README.md | 49 ++- app/Controllers/userController.php | 19 ++ app/Models/EntryDAO.php | 2 +- app/Models/FeedDAO.php | 5 +- cli/prepare.php | 1 + config-user.default.php | 2 + data/fever/.gitignore | 1 + data/fever/index.html | 13 + docs/en/users/06_Fever_API.md | 110 +++++++ docs/fr/users/06_Fever_API.md | 17 + p/api/fever.php | 634 +++++++++++++++++++++++++++++++++++++ p/api/greader.php | 2 + p/api/index.php | 11 + 14 files changed, 888 insertions(+), 27 deletions(-) create mode 100644 data/fever/.gitignore create mode 100644 data/fever/index.html create mode 100644 docs/en/users/06_Fever_API.md create mode 100644 docs/fr/users/06_Fever_API.md create mode 100644 p/api/fever.php (limited to 'app/Models/FeedDAO.php') diff --git a/README.fr.md b/README.fr.md index 97ffafb56..265030546 100644 --- a/README.fr.md +++ b/README.fr.md @@ -1,3 +1,6 @@ +[![Build Status][travis-badge]][travis-link] + +* Lire ce document sur [github.com/FreshRSS/FreshRSS/](https://github.com/FreshRSS/FreshRSS/blob/master/README.md) pour avoir les images et liens corrects. * [English version](README.md) # FreshRSS @@ -54,6 +57,8 @@ Nous sommes une communauté amicale. 6. Des paramètres de configuration avancée peuvent être vues dans [config.default.php](config.default.php) et modifiées dans `data/config.php`. 7. Avec Apache, activer [`AllowEncodedSlashes`](https://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes) pour une meilleure compatibilité avec les clients mobiles. +Plus d’informations sur l’installation et la configuration serveur peuvent être trouvées dans [notre documentation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.md). + ## Installation automatisée * [Docker](./Docker/) * [![Cloudron](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=org.freshrss.cloudronapp) @@ -107,6 +112,8 @@ sudo git pull sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/ ``` +Voir la [documentation de la ligne de commande](cli/README.md) pour plus de détails. + ## Contrôle d’accès Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix : * En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.5+ recommandé) @@ -150,11 +157,42 @@ mysqldump --skip-comments --disable-keys --user= --password --host --password --host feverKey)) { + return @unlink(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::$system_conf->salt) . '-' . $userConfig->feverKey . '.txt'); + } + return false; + } + public static function updateUser($user, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) { $userConfig = get_user_configuration($user); if ($userConfig === null) { @@ -58,6 +66,16 @@ class FreshRSS_user_Controller extends Minz_ActionController { if ($apiPasswordPlain != '') { $apiPasswordHash = self::hashPassword($apiPasswordPlain); $userConfig->apiPasswordHash = $apiPasswordHash; + + @mkdir(DATA_PATH . '/fever/', 0770, true); + self::deleteFeverKey($user); + $userConfig->feverKey = strtolower(md5($user . ':' . $apiPasswordPlain)); + $ok = file_put_contents(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::$system_conf->salt) . '-' . $userConfig->feverKey . '.txt', $user) !== false; + + if (!$ok) { + Minz_Log::warning('Could not save API credentials for fever API', ADMIN_LOG); + return $ok; + } } if (is_array($userConfigUpdated)) { @@ -258,6 +276,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $ok &= $userDAO->deleteUser($username); $ok &= recursive_unlink($user_data); array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt')); + self::deleteFeverKey(); } return $ok; } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 516aad3b8..a3bca3727 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -801,7 +801,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'WHERE ' . $where . $search . 'ORDER BY e.id ' . $order - . ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ + . ($limit > 0 ? ' LIMIT ' . intval($limit) : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ } public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) { diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index f968ae98b..9d980c139 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -299,13 +299,14 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { /** * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL. */ - public function listFeedsOrderUpdate($defaultCacheDuration = 3600) { + public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) { $this->updateTTL(); $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes ' . 'FROM `' . $this->prefix . 'feed` ' . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT . ' AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ') - . 'ORDER BY `lastUpdate`'; + . 'ORDER BY `lastUpdate` ' + . ($limit < 1 ? '' : 'LIMIT ' . intval($limit)); $stm = $this->bd->prepare($sql); if ($stm && $stm->execute()) { return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); diff --git a/cli/prepare.php b/cli/prepare.php index 2db2da555..81fb53f85 100755 --- a/cli/prepare.php +++ b/cli/prepare.php @@ -7,6 +7,7 @@ $dirs = array( '/cache', '/extensions-data', '/favicons', + '/fever', '/PubSubHubbub', '/PubSubHubbub/feeds', '/PubSubHubbub/keys', diff --git a/config-user.default.php b/config-user.default.php index 5e67d8d9b..6aef0dc49 100644 --- a/config-user.default.php +++ b/config-user.default.php @@ -9,6 +9,8 @@ return array ( 'token' => '', 'passwordHash' => '', 'apiPasswordHash' => '', + //feverKey is md5($user . ':' . $apiPasswordPlain) + 'feverKey' => '', 'posts_per_page' => 20, 'since_hours_posts_per_rss' => 168, 'min_posts_per_rss' => 2, diff --git a/data/fever/.gitignore b/data/fever/.gitignore new file mode 100644 index 000000000..2211df63d --- /dev/null +++ b/data/fever/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/data/fever/index.html b/data/fever/index.html new file mode 100644 index 000000000..85faaa37e --- /dev/null +++ b/data/fever/index.html @@ -0,0 +1,13 @@ + + + + + +Redirection + + + + +

Redirection

+ + diff --git a/docs/en/users/06_Fever_API.md b/docs/en/users/06_Fever_API.md new file mode 100644 index 000000000..58e986a64 --- /dev/null +++ b/docs/en/users/06_Fever_API.md @@ -0,0 +1,110 @@ +# FreshRSS - Fever API implementation + +## RSS clients + +There are many RSS clients existing supporting Fever APIs but they seem to understand the Fever API a bit differently. +If your favourite client does not work properly with this API, create an issue and we will have a look. +But we can **only** do that for free clients. + +### Usage & Authentication + +Before you can start to use this API, you have to enable and setup API access, which is [documented here](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html), +and then re-set the user’s API password. + +Then point your mobile application to the URL of `fever.php` (e.g. `https://freshrss.example.net/api/fever.php`). + +## Compatibility + +Tested with: + +- iOS + - [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) + - [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) + +- MacOS + - [Readkit](https://itunes.apple.com/app/readkit/id588726889) + +- Android + -Until now, we don't know about compatible Android clients. Please leave your feedback, if you tested the Fever API with Android apps. + - Please note, that *Press* is NOT compatible: it was a popular RSS client with Fever support, but its development stopped a while ago. It uses the Fever API in a wrong way, which we don't support. + +## Features + +Following features are implemented: + +- fetching categories +- fetching feeds +- fetching RSS items (new, favorites, unread, by_id, by_feed, by_category, since) +- fetching favicons +- setting read marker for item(s) +- 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 + +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 + +## Testing and error search + +If this API does not work as expected in your RSS reader, you can test it manually with a tool like [Postman](https://www.getpostman.com/). + +Configure a POST request to the URL https://freshrss.example.net/api/fever.php?api which should give you the result: +```json +{ + "api_version": 3, + "auth": 0 +} +``` +Great, the base setup seems to work! + +Now lets try an authenticated call. Fever uses an `api_key`, which is the MD5 hash of `"$username:$apiPassword"`. +Assuming the user is `kevin` and the password `freshrss`, here is a command-line example to compute the resulting `api_key` + +```sh +api_key=`echo -n "kevin:freshrss" | md5sum | cut -d' ' -f1` +``` + +Add a body to your POST request encoded as `form-data` and one key named `api_key` with the value `your-password-hash`: + +```sh +curl -s -F "api_key=$api_key" 'https://freshrss.example.net/api/fever.php?api' +``` + +This shoud give: +```json +{ + "api_version": 3, + "auth": 1, + "last_refreshed_on_time": "1520013061" +} +``` +Perfect, you are authenticated and can now start testing the more advanced features. Therefor change the URL and append the possible API actions to your request parameters. Check the [original Fever documentation](https://feedafever.com/api) for more infos. + +Some basic calls are: + +- https://freshrss.example.net/api/fever.php?api&items +- https://freshrss.example.net/api/fever.php?api&feeds +- https://freshrss.example.net/api/fever.php?api&groups +- https://freshrss.example.net/api/fever.php?api&unread_item_ids +- https://freshrss.example.net/api/fever.php?api&saved_item_ids +- https://freshrss.example.net/api/fever.php?api&items&since_id=some_id +- https://freshrss.example.net/api/fever.php?api&items&max_id=some_id +- https://freshrss.example.net/api/fever.php?api&mark=item&as=read&id=some_id +- https://freshrss.example.net/api/fever.php?api&mark=item&as=unread&id=some_id + +Replace `some_id` with a real ID from your `freshrss_username_entry` database. + +### Debugging + +If nothing helps and your clients still misbehaves, add these lines to the start of `fever.api`: + +```php +file_put_contents(__DIR__ . '/fever.log', $_SERVER['HTTP_USER_AGENT'] . ': ' . json_encode($_REQUEST) . PHP_EOL, FILE_APPEND); +``` + +Then use your RSS client to query the API and afterwards check the file `fever.log`. + +## Credits + +This plugin was inspired by the [tinytinyrss-fever-plugin](https://github.com/dasmurphy/tinytinyrss-fever-plugin). diff --git a/docs/fr/users/06_Fever_API.md b/docs/fr/users/06_Fever_API.md new file mode 100644 index 000000000..f9dcd5d73 --- /dev/null +++ b/docs/fr/users/06_Fever_API.md @@ -0,0 +1,17 @@ +# FreshRSS - API compatible Fever + + +## Compatibilité + +Testé avec: + +- iOS + - [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303) + - [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153) + +- MacOS + - [Readkit](https://itunes.apple.com/app/readkit/id588726889) + +## TODO + +Voir [la page en anglais](../../en/users/06_Fever_API.md). diff --git a/p/api/fever.php b/p/api/fever.php new file mode 100644 index 000000000..749116183 --- /dev/null +++ b/p/api/fever.php @@ -0,0 +1,634 @@ +api_enabled) { + Minz_Log::warning('serviceUnavailable() ' . debugInfo(), API_LOG); + header('HTTP/1.1 503 Service Unavailable'); + header('Content-Type: text/plain; charset=UTF-8'); + die('Service Unavailable!'); +} + +ini_set('session.use_cookies', '0'); +register_shutdown_function('session_destroy'); +Minz_Session::init('FreshRSS'); +// ================================================================================================ + + +class FeverAPI_EntryDAO extends FreshRSS_EntryDAO +{ + /** + * @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 + * @param array $bindArray + * @return string + */ + protected function bindParamArray($prefix, $values, &$bindArray) + { + $str = ''; + for ($i = 0; $i < count($values); $i++) { + $str .= ':' . $prefix . $i . ','; + $bindArray[$prefix . $i] = $values[$i]; + } + return rtrim($str, ','); + } + + /** + * @param array $feed_ids + * @param array $entry_ids + * @param int|null $max_id + * @param int|null $since_id + * @return FreshRSS_Entry[] + */ + public function findEntries(array $feed_ids, array $entry_ids, $max_id, $since_id) + { + $values = array(); + $order = ''; + + $sql = 'SELECT id, guid, title, author, ' + . ($this->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); + $sql .= " id IN($bindEntryIds)"; + } else if (!empty($max_id)) { + $sql .= ' id < :id'; + $values[':id'] = $max_id; + $order = ' ORDER BY id DESC'; + } else { + $sql .= ' id > :id'; + $values[':id'] = $since_id; + $order = ' ORDER BY id ASC'; + } + + if (!empty($feed_ids)) { + $bindFeedIds = $this->bindParamArray("feed", $feed_ids, $values); + $sql .= " AND id_feed IN($bindFeedIds)"; + } + + $sql .= $order; + $sql .= ' LIMIT 50'; + + $stm = $this->bd->prepare($sql); + $stm->execute($values); + $result = $stm->fetchAll(PDO::FETCH_ASSOC); + + $entries = array(); + foreach ($result as $dao) { + $entries[] = self::daoToEntry($dao); + } + + return $entries; + } +} + +/** + * Class FeverAPI + */ +class FeverAPI +{ + const API_LEVEL = 3; + const STATUS_OK = 1; + const STATUS_ERR = 0; + + /** + * Authenticate the user + * + * API Password sent from client is the result of the md5 sum of + * your FreshRSS "username:your-api-password" combination + */ + private function authenticate() + { + FreshRSS_Context::$user_conf = null; + Minz_Session::_param('currentUser'); + $feverKey = empty($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128); + if (ctype_xdigit($feverKey)) { + $feverKey = strtolower($feverKey); + $username = @file_get_contents(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::$system_conf->salt) . '-' . $feverKey . '.txt', false); + if ($username != false) { + $username = trim($username); + $user_conf = get_user_configuration($username); + if ($user_conf != null && $feverKey === $user_conf->feverKey) { + FreshRSS_Context::$user_conf = $user_conf; + Minz_Session::_param('currentUser', $username); + } + } + } + } + + /** + * @return bool + */ + public function isAuthenticatedApiUser() + { + $this->authenticate(); + + if (FreshRSS_Context::$user_conf !== null) { + return true; + } + + 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 + * + * @return array + * @throws Exception + */ + public function process() + { + $response_arr = array(); + + if (!$this->isAuthenticatedApiUser()) { + 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["feeds"])) { + $response_arr["feeds"] = $this->getFeeds(); + $response_arr["feeds_groups"] = $this->getFeedsGroup(); + } + + 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["links"])) { + $response_arr["links"] = $this->getLinks(); + } + + 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["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"])) { + case 'item': + $this->{$method_name}($id); + break; + case 'feed': + case 'group': + $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(); + break; + + case 'saved': + case 'unsaved': + $response_arr["saved_item_ids"] = $this->getSavedItemIds(); + break; + } + } + } + + return $response_arr; + } + + /** + * Returns the complete JSON, with 'api_version' and status as 'auth'. + * + * @param int $status + * @param array $reply + * @return string + */ + public function wrap($status, array $reply = array()) + { + $arr = array('api_version' => self::API_LEVEL, 'auth' => $status); + + if ($status === self::STATUS_OK) { + $arr['last_refreshed_on_time'] = (string) $this->lastRefreshedOnTime(); + $arr = array_merge($arr, $reply); + } + + return json_encode($arr); + } + + /** + * every authenticated method includes last_refreshed_on_time + * + * @return int + */ + protected function lastRefreshedOnTime() + { + $lastUpdate = 0; + + $dao = $this->getDaoForFeeds(); + $entries = $dao->listFeedsOrderUpdate(-1, 1); + $feed = current($entries); + + if (!empty($feed)) { + $lastUpdate = $feed->lastUpdate(); + } + + return $lastUpdate; + } + + /** + * @return array + */ + protected function getFeeds() + { + $feeds = array(); + + $dao = $this->getDaoForFeeds(); + $myFeeds = $dao->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() + ); + } + + return $feeds; + } + + /** + * @return array + */ + protected function getGroups() + { + $groups = array(); + + $dao = $this->getDaoForCategories(); + $categories = $dao->listCategories(false, false); + + /** @var FreshRSS_Category $category */ + foreach ($categories as $category) { + $groups[] = array( + 'id' => $category->id(), + 'title' => $category->name() + ); + } + + return $groups; + } + + /** + * @return array + */ + protected function getFavicons() + { + $favicons = array(); + + $dao = $this->getDaoForFeeds(); + $myFeeds = $dao->listFeeds(); + + $salt = FreshRSS_Context::$system_conf->salt; + + /** @var FreshRSS_Feed $feed */ + foreach ($myFeeds as $feed) { + + $id = hash('crc32b', $salt . $feed->url()); + $filename = DATA_PATH . '/favicons/' . $id . '.ico'; + if (!file_exists($filename)) { + continue; + } + + $favicons[] = array( + "id" => $feed->id(), + "data" => image_type_to_mime_type(exif_imagetype($filename)) . ";base64," . base64_encode(file_get_contents($filename)) + ); + } + + return $favicons; + } + + /** + * @return int + */ + protected function getTotalItems() + { + $total_items = 0; + + $dao = $this->getDaoForEntries(); + $result = $dao->countFever(); + + if (!empty($result)) { + $total_items = $result['total']; + } + + return $total_items; + } + + /** + * @return array + */ + protected function getFeedsGroup() + { + $groups = array(); + $ids = array(); + + $dao = $this->getDaoForFeeds(); + $myFeeds = $dao->listFeeds(); + + /** @var FreshRSS_Feed $feed */ + foreach ($myFeeds as $feed) { + $ids[$feed->category()][] = $feed->id(); + } + + foreach($ids as $category => $feedIds) { + $groups[] = array( + 'group_id' => $category, + 'feed_ids' => implode(',', $feedIds) + ); + } + + return $groups; + } + + /** + * AFAIK there is no 'hot links' alternative in FreshRSS + * @return array + */ + protected function getLinks() + { + return array(); + } + + /** + * @param array $ids + * @return string + */ + protected function entriesToIdList($ids = array()) + { + return implode(',', array_values($ids)); + } + + /** + * @return string + */ + protected function getUnreadItemIds() + { + $dao = $this->getDaoForEntries(); + $entries = $dao->listIdsWhere('a', '', FreshRSS_Entry::STATE_NOT_READ, 'ASC', 0); + return $this->entriesToIdList($entries); + } + + /** + * @return string + */ + protected function getSavedItemIds() + { + $dao = $this->getDaoForEntries(); + $entries = $dao->listIdsWhere('a', '', FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0); + return $this->entriesToIdList($entries); + } + + protected function setItemAsRead($id) + { + $dao = $this->getDaoForEntries(); + $dao->markRead($id, true); + } + + protected function setItemAsUnread($id) + { + $dao = $this->getDaoForEntries(); + $dao->markRead($id, false); + } + + protected function setItemAsSaved($id) + { + $dao = $this->getDaoForEntries(); + $dao->markFavorite($id, true); + } + + protected function setItemAsUnsaved($id) + { + $dao = $this->getDaoForEntries(); + $dao->markFavorite($id, false); + } + + /** + * @return array + */ + protected function getItems() + { + $feed_ids = array(); + $entry_ids = array(); + $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"]); + } + + $dao = $this->getDaoForCategories(); + if (isset($_REQUEST["group_ids"])) { + $group_ids = explode(",", $_REQUEST["group_ids"]); + foreach ($group_ids as $id) { + /** @var FreshRSS_Category $category */ + $category = $dao->searchById($id); + /** @var FreshRSS_Feed $feed */ + foreach ($category->feeds() as $feed) { + $feeds[] = $feed->id(); + } + } + + $feed_ids = array_unique($feeds); + } + } + + 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 ($max) { + $max_id = $max; + } + } + } 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; + } + + $items = array(); + + $dao = $this->getDaoForEntries(); + $entries = $dao->findEntries($feed_ids, $entry_ids, $max_id, $since_id); + + // Load list of extensions and enable the "system" ones. + Minz_ExtensionManager::init(); + + foreach($entries as $item) { + /** @var FreshRSS_Entry $entry */ + $entry = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($entry)) { + 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) + ); + } + + return $items; + } + + /** + * TODO replace by a dynamic fetch for id <= $before timestamp + * + * @param int $beforeTimestamp + * @return int + */ + 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; + } + + protected function setFeedAsRead($id, $before) + { + $before = $this->convertBeforeToId($before); + $dao = $this->getDaoForEntries(); + return $dao->markReadFeed($id, $before); + } + + protected function setGroupAsRead($id, $before) + { + $dao = $this->getDaoForEntries(); + + // special case to mark all items as read + if ($id === 0) { + $result = $dao->countFever(); + + if (!empty($result)) { + return $dao->markReadEntries($result['max']); + } + } + + $before = $this->convertBeforeToId($before); + return $dao->markReadCat($id, $before); + } +} + +// ================================================================================================ +// refresh is not allowed yet, probably we find a way to support it later +if (isset($_REQUEST["refresh"])) { + Minz_Log::warning('Refresh items for fever API - notImplemented()', API_LOG); + header('HTTP/1.1 501 Not Implemented'); + header('Content-Type: text/plain; charset=UTF-8'); + die('Not Implemented!'); +} + +// Start the Fever API handling +$handler = new FeverAPI(); + +header("Content-Type: application/json; charset=UTF-8"); + +if (!$handler->isAuthenticatedApiUser()) { + echo $handler->wrap(FeverAPI::STATUS_ERR, array()); +} else { + echo $handler->wrap(FeverAPI::STATUS_OK, $handler->process()); +} diff --git a/p/api/greader.php b/p/api/greader.php index 2a32ead4e..5ab6c8115 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -745,6 +745,8 @@ if (!FreshRSS_Context::$system_conf->api_enabled) { serviceUnavailable(); } +ini_set('session.use_cookies', '0'); +register_shutdown_function('session_destroy'); Minz_Session::init('FreshRSS'); $user = authorizationToUser(); diff --git a/p/api/index.php b/p/api/index.php index 429b25225..108841819 100644 --- a/p/api/index.php +++ b/p/api/index.php @@ -26,5 +26,16 @@ echo Minz_Url::display('/api/greader.php', 'html', true); configuration (without %2F support) +

Fever compatible API

+
+
Your API address:
+
+
+ + -- cgit v1.2.3