aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.fr.md49
-rw-r--r--README.md49
-rw-r--r--app/Controllers/userController.php19
-rw-r--r--app/Models/EntryDAO.php2
-rw-r--r--app/Models/FeedDAO.php5
-rwxr-xr-xcli/prepare.php1
-rw-r--r--config-user.default.php2
-rw-r--r--data/fever/.gitignore1
-rw-r--r--data/fever/index.html13
-rw-r--r--docs/en/users/06_Fever_API.md110
-rw-r--r--docs/fr/users/06_Fever_API.md17
-rw-r--r--p/api/fever.php634
-rw-r--r--p/api/greader.php2
-rw-r--r--p/api/index.php11
14 files changed, 888 insertions, 27 deletions
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=<db_user> --password --host <db_
```
-# Extensions
+# Extensions
FreshRSS permet l’ajout d’extensions en plus des fonctionnalités natives.
Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensions).
+# APIs et applications natives
+
+FreshRSS supporte l’accès depuis des applications native pour Linux, Android, iOS, et OS X, grâce à deux APIs distinctes.
+
+## Via l’API compatible Google Reader
+
+Voir notre [documentation sur l’accès mobile](https://freshrss.github.io/FreshRSS/fr/users/06_Mobile_access.html).
+
+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)
+
+## Via l’API compatible Fever
+
+Voir notre [documentation sur l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.html) page.
+
+Tout client supportant une API de type Fever ; Sélection :
+
+* 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)
+
+
# Bibliothèques incluses
* [SimplePie](https://simplepie.org/)
* [MINZ](https://github.com/marienfressinaud/MINZ)
@@ -174,12 +212,3 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
* [password_compat](https://github.com/ircmaxell/password_compat)
-# [Clients compatibles](https://freshrss.github.io/FreshRSS/fr/users/06_Mobile_access.html)
-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 cacf61fa9..0bd4f59c0 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ We are a friendly community.
6. Advanced configuration settings can be seen in [config.default.php](config.default.php) and modified in `data/config.php`.
7. When using Apache, enable [`AllowEncodedSlashes`](https://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes) for better compatibility with mobile clients.
-More information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html).
+More information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html).
## Automated install
* [Docker](./Docker/)
@@ -111,6 +111,7 @@ cd /usr/share/FreshRSS
sudo git pull
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
```
+
See more commands and git commands in the [Command-Line Interface documentation](cli/README.md).
## Access control
@@ -156,9 +157,40 @@ mysqldump --skip-comments --disable-keys --user=<db_user> --password --host <db_
```
-# Extensions
+# Extensions
FreshRSS supports further customizations by adding extensions on top of its core functionality.
-See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions).
+See the [repository dedicated to those extensions](https://github.com/FreshRSS/Extensions).
+
+
+# APIs & native apps
+
+FreshRSS supports access from native apps for Linux, Android, iOS, and OS X, via two distinct APIs.
+
+## Google Reader-like API
+
+There is more information available about our Google Reader compatible API on the page [mobile access](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html).
+
+Supported clients are:
+
+* 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)
+
+## Fever API
+
+See our [Fever API documentation](https://freshrss.github.io/FreshRSS/en/users/06_Fever_API.html) page.
+
+Supported clients are:
+
+* 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)
# Included libraries
@@ -179,16 +211,5 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
* [Services_JSON](https://pear.php.net/pepr/pepr-proposal-show.php?id=198)
* [password_compat](https://github.com/ircmaxell/password_compat)
-
-# [Compatible clients](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html)
-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)
-
[travis-badge]:https://travis-ci.org/FreshRSS/FreshRSS.svg?branch=master
[travis-link]:https://travis-ci.org/FreshRSS/FreshRSS
diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php
index 4b47b365e..e09572b7e 100644
--- a/app/Controllers/userController.php
+++ b/app/Controllers/userController.php
@@ -44,6 +44,14 @@ class FreshRSS_user_Controller extends Minz_ActionController {
return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
}
+ public static function deleteFeverKey($username) {
+ $userConfig = get_user_configuration($username);
+ if ($userConfig !== null && ctype_xdigit($userConfig->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 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">
+<head>
+<meta charset="UTF-8" />
+<meta http-equiv="Refresh" content="0; url=/" />
+<title>Redirection</title>
+<meta name="robots" content="noindex" />
+</head>
+
+<body>
+<p><a href="/">Redirection</a></p>
+</body>
+</html>
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 @@
+<?php
+/**
+ * Fever API for FreshRSS
+ * Version 0.1
+ * Author: Kevin Papst / https://github.com/kevinpapst
+ *
+ * Inspired by:
+ * TinyTinyRSS Fever API plugin @dasmurphy
+ * See https://github.com/dasmurphy/tinytinyrss-fever-plugin
+ */
+
+// ================================================================================================
+// BOOTSTRAP FreshRSS
+require(__DIR__ . '/../../constants.php');
+require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
+Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
+
+// check if API is enabled globally
+FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+if (!FreshRSS_Context::$system_conf->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 <code>%2F</code> support)</a></li>
</ul>
+<h2>Fever compatible API</h2>
+<dl>
+<dt>Your API address:</dt>
+<dd><?php
+echo Minz_Url::display('/api/fever.php', 'html', true);
+?></dd>
+</dl>
+<ul>
+<li><a href="fever.php?api" rel="nofollow">Test</a></li>
+</ul>
+
</body>
</html>