From 8c2113f9e6eb86b630a4e861513229d7abf219b8 Mon Sep 17 00:00:00 2001 From: Alexis Degrugillier Date: Mon, 1 Jan 2018 20:34:06 +0100 Subject: Add mute strategy configuration (#1750) --- app/Models/EntryDAO.php | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index e8b6dcdae..9e291f4ed 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -726,23 +726,23 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $values = array(); switch ($type) { case 'a': - $where .= 'f.priority > 0 '; - $joinFeed = true; + $where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; break; case 's': //Deprecated: use $state instead - $where .= 'e.is_favorite=1 '; + $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; + $where .= 'AND e.is_favorite=1 '; break; case 'c': - $where .= 'f.category=? '; + $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; + $where .= 'AND f.category=? '; $values[] = intval($id); - $joinFeed = true; break; case 'f': $where .= 'e.id_feed=? '; $values[] = intval($id); break; case 'A': - $where .= '1=1 '; + $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; break; default: throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!'); @@ -752,7 +752,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return array(array_merge($values, $searchValues), 'SELECT e.id FROM `' . $this->prefix . 'entry` e ' - . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id ' : '') + . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'WHERE ' . $where . $search . 'ORDER BY e.id ' . $order @@ -873,12 +873,28 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } public function countUnreadReadFavorites() { - $sql = 'SELECT c FROM (' - . 'SELECT COUNT(id) AS c, 1 as o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 ' - . 'UNION SELECT COUNT(id) AS c, 2 AS o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 AND is_read=0' - . ') u ORDER BY o'; + $sql = <<prefix}entry` AS e1 + JOIN `{$this->prefix}feed` AS f1 ON e1.id_feed = f1.id + WHERE e1.is_favorite = 1 + AND f1.priority >= :priority_normal + UNION + SELECT COUNT(e2.id) AS c + , 2 AS o + FROM `{$this->prefix}entry` AS e2 + JOIN `{$this->prefix}feed` AS f2 ON e2.id_feed = f2.id + WHERE e2.is_favorite = 1 + AND e2.is_read = 0 + AND f2.priority >= :priority_normal + ) u +ORDER BY o +SQL; $stm = $this->bd->prepare($sql); - $stm->execute(); + $stm->execute(array(':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL)); $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); $all = empty($res[0]) ? 0 : $res[0]; $unread = empty($res[1]) ? 0 : $res[1]; -- cgit v1.2.3 From 79f8b440d1303a0cd377cabe18750a2a552919e3 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Thu, 8 Feb 2018 20:11:05 +0100 Subject: API /reader/api/0/stream/items/contents (#1774) * API /reader/api/0/stream/items/contents For FeedMe * Fix continuation * Continuation in stream/items/ids * Fix multiple continuations * Allow empty POST tokens For FeedMe. This token is not used by e.g. The Old Reader API. There is the Authorization header anyway. TODO: Check security consequences * API compatibility FeedMe: add/remove feed FeedMe uses GET for some parameters typically given by POST * A bit of sanitization * Links to FeedMe * API favicons more robust when base_url is not set * Changelog FeedMe --- CHANGELOG.md | 4 +- README.fr.md | 1 + README.md | 1 + app/Models/EntryDAO.php | 23 ++++- docs/en/users/06_Mobile_access.md | 1 + docs/fr/users/06_Mobile_access.md | 1 + p/api/greader.php | 179 ++++++++++++++++++++++++++------------ 7 files changed, 149 insertions(+), 61 deletions(-) (limited to 'app/Models/EntryDAO.php') diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed835205..2908cd616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # FreshRSS changelog -## 2018-XX-XX FreshRSS 1.9.1-dev +## 2018-02-XX FreshRSS 1.9.1-dev +* API + * Add compatibility with FeedMe 3.5.3+ on Android [#1774](https://github.com/FreshRSS/FreshRSS/pull/1774) * Features * Ability to pause feeds, and to hide them from categories [#1750](https://github.com/FreshRSS/FreshRSS/pull/1750) * Security diff --git a/README.fr.md b/README.fr.md index b888f7738..4421f92f0 100644 --- a/README.fr.md +++ b/README.fr.md @@ -177,6 +177,7 @@ Tout client supportant une API de type Google Reader. Sélection : * Android * [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) avec [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Propriétaire) + * [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Propriétaire) * [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, [F-Droid](https://f-droid.org/fr/packages/org.freshrss.easyrss/)) * GNU/Linux * [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre) diff --git a/README.md b/README.md index c971675fa..a95593651 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ Any client supporting a Google Reader-like API. Selection: * Android * [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Closed source) + * [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Closed source) * [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, [F-Droid](https://f-droid.org/packages/org.freshrss.easyrss/)) * GNU/Linux * [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source) diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 9e291f4ed..70135e7a0 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -628,10 +628,12 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $firstId = $order === 'DESC' ? '9000000000'. '000000' : '0'; }*/ if ($firstId !== '') { - $search .= 'AND ' . $alias . 'id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' '; + $search .= 'AND ' . $alias . 'id ' . ($order === 'DESC' ? '<=' : '>=') . ' ? '; + $values[] = $firstId; } if ($date_min > 0) { - $search .= 'AND ' . $alias . 'id >= ' . $date_min . '000000 '; + $search .= 'AND ' . $alias . 'id >= ? '; + $values[] = $date_min . '000000'; } if ($filter) { if ($filter->getMinDate()) { @@ -781,6 +783,23 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC)); } + public function listByIds($ids, $order = 'DESC') { + if (count($ids) < 1) { + return array(); + } + + $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 id IN (' . str_repeat('?,', count($ids) - 1). '?) ' + . 'ORDER BY id ' . $order; + + $stm = $this->bd->prepare($sql); + $stm->execute($ids); + return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC)); + } + public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { //For API list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min); diff --git a/docs/en/users/06_Mobile_access.md b/docs/en/users/06_Mobile_access.md index 3472172b0..166985585 100644 --- a/docs/en/users/06_Mobile_access.md +++ b/docs/en/users/06_Mobile_access.md @@ -46,6 +46,7 @@ This page assumes you have completed the [server setup](../admins/02_Installatio 7. Pick a client supporting a Google Reader-like API. Selection: * Android * [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Closed source) + * [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Closed source) * [EasyRSS](https://github.com/Alkarex/EasyRSS) (Open source, [F-Droid](https://f-droid.org/packages/org.freshrss.easyrss/)) * Linux * [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source) diff --git a/docs/fr/users/06_Mobile_access.md b/docs/fr/users/06_Mobile_access.md index 185c94098..8ef3d038a 100644 --- a/docs/fr/users/06_Mobile_access.md +++ b/docs/fr/users/06_Mobile_access.md @@ -44,6 +44,7 @@ Tout client supportant une API de type Google Reader. Sélection : * Android * [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) avec [News+ Google Reader extension](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader) (Propriétaire) + * [FeedMe 3.5.3+](https://play.google.com/store/apps/details?id=com.seazon.feedme) (Propriétaire) * [EasyRSS](https://github.com/Alkarex/EasyRSS) (Libre, F-Droid) * Linux * [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre) diff --git a/p/api/greader.php b/p/api/greader.php index 72f886190..4add26bd8 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -216,9 +216,13 @@ function token($conf) { function checkToken($conf, $token) { //http://code.google.com/p/google-reader-api/wiki/ActionToken $user = Minz_Session::param('currentUser', '_'); + if ($user !== '_' && $token == '') { + return true; //FeedMe //TODO: Check security consequences + } if ($token === str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) { return true; } + Minz_Log::warning('Invalid POST token: ' . $token, API_LOG); unauthorized(); } @@ -266,6 +270,8 @@ function subscriptionList() { $res = $stm->fetchAll(PDO::FETCH_ASSOC); $salt = FreshRSS_Context::$system_conf->salt; + $faviconsUrl = Minz_Url::display('/f.php?', '', true); + $faviconsUrl = str_replace('/api/greader.php/reader/api/0/subscription', '', $faviconsUrl); //Security if base_url is not set properly $subscriptions = array(); foreach ($res as $line) { @@ -282,7 +288,7 @@ function subscriptionList() { //'firstitemmsec' => 0, 'url' => $line['url'], 'htmlUrl' => $line['website'], - 'iconUrl' => Minz_Url::display('/f.php?' . hash('crc32b', $salt . $line['url']), '', true), + 'iconUrl' => $faviconsUrl . hash('crc32b', $salt . $line['url']), ); } @@ -324,6 +330,9 @@ function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = ' $addCatId = 1; //Default category } $feedDAO = FreshRSS_Factory::createFeedDao(); + if (!is_array($streamNames) || count($streamNames) < 1) { + badRequest(); + } for ($i = count($streamNames) - 1; $i >= 0; $i--) { $streamName = $streamNames[$i]; //feed/http://example.net/sample.xml ; feed/338 if (strpos($streamName, 'feed/') === 0) { @@ -435,6 +444,51 @@ function unreadCount() { //http://blog.martindoms.com/2009/10/16/using-the-googl exit(); } +function entriesToArray($entries) { + $items = array(); + foreach ($entries as $entry) { + $f_id = $entry->feed(); + if (isset($arrayFeedCategoryNames[$f_id])) { + $c_name = $arrayFeedCategoryNames[$f_id]['c_name']; + $f_name = $arrayFeedCategoryNames[$f_id]['name']; + } else { + $c_name = '_'; + $f_name = '_'; + } + $item = array( + 'id' => /*'tag:google.com,2005:reader/item/' .*/ dec2hex($entry->id()), //64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId + 'crawlTimeMsec' => substr($entry->id(), 0, -3), + 'timestampUsec' => '' . $entry->id(), //EasyRSS + 'published' => $entry->date(true), + 'title' => $entry->title(), + 'summary' => array('content' => $entry->content()), + 'alternate' => array( + array('href' => htmlspecialchars_decode($entry->link(), ENT_QUOTES)), + ), + 'categories' => array( + 'user/-/state/com.google/reading-list', + 'user/-/label/' . $c_name, + ), + 'origin' => array( + 'streamId' => 'feed/' . $f_id, + 'title' => $f_name, //EasyRSS + //'htmlUrl' => $line['f_website'], + ), + ); + if ($entry->author() != '') { + $item['author'] = $entry->author(); + } + if ($entry->isRead()) { + $item['categories'][] = 'user/-/state/com.google/read'; + } + if ($entry->isFavorite()) { + $item['categories'][] = 'user/-/state/com.google/starred'; + } + $items[] = $item; + } + return $items; +} + function streamContents($path, $include_target, $start_time, $count, $order, $exclude_target, $continuation) { //http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed @@ -476,57 +530,18 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex break; } - if (!empty($continuation)) { + if ($continuation != '') { $count++; //Shift by one element } $entryDAO = FreshRSS_Factory::createEntryDao(); $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_Search(''), $start_time); - $items = array(); - foreach ($entries as $entry) { - $f_id = $entry->feed(); - if (isset($arrayFeedCategoryNames[$f_id])) { - $c_name = $arrayFeedCategoryNames[$f_id]['c_name']; - $f_name = $arrayFeedCategoryNames[$f_id]['name']; - } else { - $c_name = '_'; - $f_name = '_'; - } - $item = array( - 'id' => /*'tag:google.com,2005:reader/item/' .*/ dec2hex($entry->id()), //64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId - 'crawlTimeMsec' => substr($entry->id(), 0, -3), - 'timestampUsec' => '' . $entry->id(), //EasyRSS - 'published' => $entry->date(true), - 'title' => $entry->title(), - 'summary' => array('content' => $entry->content()), - 'alternate' => array( - array('href' => htmlspecialchars_decode($entry->link(), ENT_QUOTES)), - ), - 'categories' => array( - 'user/-/state/com.google/reading-list', - 'user/-/label/' . $c_name, - ), - 'origin' => array( - 'streamId' => 'feed/' . $f_id, - 'title' => $f_name, //EasyRSS - //'htmlUrl' => $line['f_website'], - ), - ); - if ($entry->author() != '') { - $item['author'] = $entry->author(); - } - if ($entry->isRead()) { - $item['categories'][] = 'user/-/state/com.google/read'; - } - if ($entry->isFavorite()) { - $item['categories'][] = 'user/-/state/com.google/starred'; - } - $items[] = $item; - } + $items = entriesToArray($entries); - if (!empty($continuation)) { + if ($continuation != '') { array_shift($items); //Discard first element that was already sent in the previous response + $count--; } $response = array( @@ -534,15 +549,18 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex 'updated' => time(), 'items' => $items, ); - if ((count($entries) >= $count) && (!empty($entry))) { - $response['continuation'] = $entry->id(); + if (count($entries) >= $count) { + $entry = end($entries); + if ($entry != false) { + $response['continuation'] = $entry->id(); + } } echo json_encode($response), "\n"; exit(); } -function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude_target) { +function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude_target, $continuation) { //http://code.google.com/p/google-reader-api/wiki/ApiStreamItemsIds //http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed @@ -572,8 +590,17 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude break; } + if ($continuation != '') { + $count++; //Shift by one element + } + $entryDAO = FreshRSS_Factory::createEntryDao(); - $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, '', new FreshRSS_Search(''), $start_time); + $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_Search(''), $start_time); + + if ($continuation != '') { + array_shift($ids); //Discard first element that was already sent in the previous response + $count--; + } if (empty($ids)) { //For News+ bug https://github.com/noinnion/newsplus/issues/84#issuecomment-57834632 $ids[] = 0; @@ -585,9 +612,39 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude ); } - echo json_encode(array( + $response = array( 'itemRefs' => $itemRefs, - )), "\n"; + ); + if (count($ids) >= $count) { + $id = end($ids); + if ($id != false) { + $response['continuation'] = $id; + } + } + + echo json_encode($response), "\n"; + exit(); +} + +function streamContentsItems($e_ids, $order) { + header('Content-Type: application/json; charset=UTF-8'); + + foreach ($e_ids as $i => $e_id) { + $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/' + } + + $entryDAO = FreshRSS_Factory::createEntryDao(); + $entries = $entryDAO->listByIds($e_ids, $order === 'o' ? 'ASC' : 'DESC'); + + $items = entriesToArray($entries); + + $response = array( + 'id' => 'user/-/state/com.google/reading-list', + 'updated' => time(), + 'items' => $items, + ); + + echo json_encode($response), "\n"; exit(); } @@ -726,7 +783,10 @@ if (count($pathInfos) < 3) { * all items in a timestamp range, it will have a continuation attribute. * The same request can be re-issued with the value of that attribute put * in this parameter to get more items */ - $continuation = isset($_GET['c']) ? $_GET['c'] : ''; + $continuation = isset($_GET['c']) ? trim($_GET['c']) : ''; + if (!ctype_digit($continuation)) { + $continuation = ''; + } if (isset($pathInfos[5]) && $pathInfos[5] === 'contents' && isset($pathInfos[6])) { if (isset($pathInfos[7])) { if ($pathInfos[6] === 'feed') { @@ -755,7 +815,10 @@ if (count($pathInfos) < 3) { * be repeated to fetch the item IDs from multiple streams at once * (more efficient from a backend perspective than multiple requests). */ $streamId = $_GET['s']; - streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude_target); + streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude_target, $continuation); + } else if ($pathInfos[6] === 'contents' && isset($_POST['i'])) { //FeedMe + $e_ids = multiplePosts('i'); //item IDs + streamContentsItems($e_ids, $order); } } break; @@ -775,16 +838,16 @@ if (count($pathInfos) < 3) { subscriptionList($_GET['output']); break; case 'edit': - if (isset($_POST['s']) && isset($_POST['ac'])) { + if (isset($_REQUEST['s']) && isset($_REQUEST['ac'])) { //StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once - $streamNames = multiplePosts('s'); + $streamNames = empty($_POST['s']) && isset($_GET['s']) ? array($_GET['s']) : multiplePosts('s'); /* Title to use for the subscription. For the `subscribe` action, * if not specified then the feed's current title will be used. Can * be used with the `edit` action to rename a subscription */ - $titles = multiplePosts('t'); - $action = $_POST['ac']; //Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit` - $add = isset($_POST['a']) ? $_POST['a'] : ''; //StreamId to add the subscription to (generally a user label) - $remove = isset($_POST['r']) ? $_POST['r'] : ''; //StreamId to remove the subscription from (generally a user label) + $titles = empty($_POST['t']) && isset($_GET['t']) ? array($_GET['t']) : multiplePosts('t'); + $action = $_REQUEST['ac']; //Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit` + $add = isset($_REQUEST['a']) ? $_REQUEST['a'] : ''; //StreamId to add the subscription to (generally a user label) + $remove = isset($_REQUEST['r']) ? $_REQUEST['r'] : ''; //StreamId to remove the subscription from (generally a user label) subscriptionEdit($streamNames, $titles, $action, $add, $remove); } break; -- cgit v1.2.3