diff --git a/README.markdown b/README.markdown
index 059938d..262f522 100644
--- a/README.markdown
+++ b/README.markdown
@@ -69,6 +69,7 @@ Documentation
- [Translations](docs/translations.markdown)
- [Themes](docs/themes.markdown)
- [API documentation](http://miniflux.net/api.html)
+- [Fever API](docs/fever.markdown)
- [FAQ](docs/faq.markdown)
Todo and known bugs
diff --git a/common.php b/common.php
index 00662f9..7371e90 100644
--- a/common.php
+++ b/common.php
@@ -32,7 +32,7 @@ defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20);
defined('BASE_URL_DIRECTORY') or define('BASE_URL_DIRECTORY', dirname($_SERVER['PHP_SELF']));
defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__);
-defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', 'data');
+defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', __DIR__.'/data');
defined('ENABLE_MULTIPLE_DB') or define('ENABLE_MULTIPLE_DB', true);
defined('DB_FILENAME') or define('DB_FILENAME', 'db.sqlite');
diff --git a/controllers/config.php b/controllers/config.php
index 18c2367..89089e5 100644
--- a/controllers/config.php
+++ b/controllers/config.php
@@ -74,7 +74,7 @@ Router\get_action('auto-update', function() {
Router\get_action('generate-tokens', function() {
Model\Config\new_tokens();
- Response\redirect('?action=config#api');
+ Response\redirect('?action=config');
});
// Optimize the database manually
diff --git a/docs/fever-api.markdown b/docs/fever-api.markdown
new file mode 100644
index 0000000..c41c5bb
--- /dev/null
+++ b/docs/fever-api.markdown
@@ -0,0 +1,27 @@
+Fever API
+=========
+
+Miniflux support the [Fever API](http://feedafever.com/api).
+That means you can use mobile applications compatible with Fever.
+
+This feature have been tested with the following apps:
+
+- [Press for Android](http://twentyfivesquares.com/press/)
+
+Configuration
+-------------
+
+Miniflux generates a random password for the Fever API.
+All information are available from the **preferences page**.
+
+- URL: http://your_miniflux_url/fever/
+- Username: Your username
+- Password: random (visible on the settings page)
+
+Notes
+-----
+
+- Links, sparks, kindling, favicons and groups are not supported.
+- All feeds will be under a category "All" because Miniflux doesn't support categories.
+- Only JSON responses are handled.
+- If you have multiple users with Miniflux, that will works only with the default user.
diff --git a/fever/index.php b/fever/index.php
new file mode 100644
index 0000000..2215a23
--- /dev/null
+++ b/fever/index.php
@@ -0,0 +1,318 @@
+table('config')
+ ->columns('username', 'fever_token')
+ ->findOne();
+
+ $api_key = md5($credentials['username'].':'.$credentials['fever_token']);
+
+ $response = array(
+ 'api_version' => 3,
+ 'auth' => (int) (@$_POST['api_key'] === $api_key),
+ 'last_refreshed_on_time' => time(),
+ );
+
+ return $response;
+}
+
+// Call: ?api&groups
+route('groups', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ $feed_ids = Database::get('db')
+ ->table('feeds')
+ ->findAllByColumn('id');
+
+ $response['groups'] = array(
+ array(
+ 'id' => 1,
+ 'title' => t('All'),
+ )
+ );
+
+ $response['feeds_groups'] = array(
+ array(
+ 'group_id' => 1,
+ 'feed_ids' => implode(',', $feed_ids),
+ )
+ );
+ }
+
+ response($response);
+});
+
+// Call: ?api&feeds
+route('feeds', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ $response['feeds'] = array();
+ $feeds = Feed\get_all();
+ $feed_ids = array();
+
+ foreach ($feeds as $feed) {
+ $response['feeds'][] = array(
+ 'id' => (int) $feed['id'],
+ 'favicon_id' => 1,
+ 'title' => $feed['title'],
+ 'url' => $feed['feed_url'],
+ 'site_url' => $feed['site_url'],
+ 'is_spark' => 0,
+ 'last_updated_on_time' => $feed['last_checked'] ?: time(),
+ );
+
+ $feed_ids[] = $feed['id'];
+ }
+
+ $response['feeds_groups'] = array(
+ array(
+ 'group_id' => 1,
+ 'feed_ids' => implode(',', $feed_ids),
+ )
+ );
+ }
+
+ response($response);
+});
+
+// Call: ?api&favicons
+route('favicons', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+ $response['favicons'] = array();
+ }
+
+ response($response);
+});
+
+// Call: ?api&items
+route('items', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ $offset = 0;
+ $direction = 'ASC';
+
+ if (isset($_GET['since_id']) && is_numeric($_GET['since_id'])) {
+
+ $offset = $_GET['since_id'];
+ $direction = 'ASC';
+ }
+ else if (isset($_GET['max_id']) && is_numeric($_GET['max_id'])) {
+
+ $offset = $_GET['max_id'];
+ $direction = 'DESC';
+ }
+
+ $query = Database::get('db')
+ ->table('items')
+ ->columns(
+ 'rowid',
+ 'feed_id',
+ 'title',
+ 'author',
+ 'content',
+ 'url',
+ 'updated',
+ 'status',
+ 'bookmark'
+ )
+ ->orderby('rowid', $direction)
+ ->offset($offset)
+ ->limit(50);
+
+ if (! empty($_GET['with_ids'])) {
+ $query->in('rowid', explode(',', $_GET['with_ids']));
+ }
+
+ $items = $query->findAll();
+ $response['items'] = array();
+
+ foreach ($items as $item) {
+ $response['items'][] = array(
+ 'id' => (int) $item['rowid'],
+ 'feed_id' => (int) $item['feed_id'],
+ 'title' => $item['title'],
+ 'author' => $item['author'],
+ 'html' => $item['content'],
+ 'url' => $item['url'],
+ 'is_saved' => (int) $item['bookmark'],
+ 'is_read' => $item['status'] == 'read' ? 1 : 0,
+ 'created_on_time' => $item['updated'],
+ );
+ }
+
+ $response['total_items'] = Database::get('db')
+ ->table('items')
+ ->neq('status', 'removed')
+ ->count();
+ }
+
+ response($response);
+});
+
+// Call: ?api&links
+route('links', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+ $response['links'] = array();
+ }
+
+ response($response);
+});
+
+// Call: ?api&unread_item_ids
+route('unread_item_ids', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ $item_ids = Database::get('db')
+ ->table('items')
+ ->eq('status', 'unread')
+ ->findAllByColumn('rowid');
+
+ $response['unread_item_ids'] = implode(',', $item_ids);
+ }
+
+ response($response);
+});
+
+// Call: ?api&saved_item_ids
+route('saved_item_ids', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ $item_ids = Database::get('db')
+ ->table('items')
+ ->eq('bookmark', 1)
+ ->findAllByColumn('rowid');
+
+ $response['saved_item_ids'] = implode(',', $item_ids);
+ }
+
+ response($response);
+});
+
+// handle write items
+route('write_items', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ $query = Database::get('db')
+ ->table('items')
+ ->eq('rowid', $_POST['id']);
+
+ if ($_POST['as'] === 'saved') {
+ $query->update(array('bookmark' => 1));
+ }
+ else if ($_POST['as'] === 'unsaved') {
+ $query->update(array('bookmark' => 0));
+ }
+ else if ($_POST['as'] === 'read') {
+ $query->update(array('status' => 'read'));
+ }
+ else if ($_POST['as'] === 'unread') {
+ $query->update(array('status' => 'unread'));
+ }
+ }
+
+ response($response);
+});
+
+// handle write feeds
+route('write_feeds', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ Database::get('db')
+ ->table('items')
+ ->eq('feed_id', $_POST['id'])
+ ->lte('updated', $_POST['before'])
+ ->update(array('status' => $_POST['as'] === 'read' ? 'read' : 'unread'));
+ }
+
+ response($response);
+});
+
+// handle write groups
+route('write_groups', function() {
+
+ $response = auth();
+
+ if ($response['auth']) {
+
+ Database::get('db')
+ ->table('items')
+ ->lte('updated', $_POST['before'])
+ ->update(array('status' => $_POST['as'] === 'read' ? 'read' : 'unread'));
+ }
+
+ response($response);
+});
+
+foreach (array_keys($_GET) as $action) {
+ route($action);
+}
+
+if (! empty($_POST['mark']) && ! empty($_POST['as']) && ! empty($_POST['id'])) {
+
+ if ($_POST['mark'] === 'item') {
+ route('write_items');
+ }
+ else if ($_POST['mark'] === 'feed' && ! empty($_POST['before'])) {
+ route('write_feeds');
+ }
+ else if ($_POST['mark'] === 'group' && ! empty($_POST['before'])) {
+ route('write_groups');
+ }
+}
+
+response(auth());
diff --git a/locales/cs_CZ/translations.php b/locales/cs_CZ/translations.php
index 149aa79..c2d034c 100644
--- a/locales/cs_CZ/translations.php
+++ b/locales/cs_CZ/translations.php
@@ -96,14 +96,6 @@ return array(
'Never' => 'Nikdy',
'After %d day' => 'Po %d dni',
'After %d days' => 'Po %d dnech',
- 'French' => 'Francouzština',
- 'English' => 'Angličtina',
- 'German' => 'Němčina',
- 'Italian' => 'Italština',
- // 'Spanish' => '',
- 'Simplified Chinese' => 'Zjednodušená čínština',
- 'Czech' => 'Čeština',
- // 'Portuguese' => '',
'unread' => 'nepřečtené',
'bookmark' => 'přidat do záložek',
'remove bookmark' => 'odstranit záložku',
@@ -230,4 +222,7 @@ return array(
// 'Remove this feed' => '',
// 'Miniflux' => '',
// 'miniflux' => '',
+ // 'Username:' => '',
+ // 'Password:' => '',
+ // 'All' => '',
);
diff --git a/locales/de_DE/translations.php b/locales/de_DE/translations.php
index 39b6b26..b5ed73c 100644
--- a/locales/de_DE/translations.php
+++ b/locales/de_DE/translations.php
@@ -96,14 +96,6 @@ return array(
'Never' => 'Niemals',
'After %d day' => 'Nach %d Tag',
'After %d days' => 'Nach %d Tagen',
- 'French' => 'Französisch',
- 'English' => 'Englisch',
- 'German' => 'Deutsch',
- 'Italian' => 'Italienisch',
- 'Spanish' => 'Spanisch',
- 'Simplified Chinese' => 'Vereinfachtes Chinesisch',
- 'Czech' => 'Tschechisch',
- 'Portuguese' => 'Portugiesisch',
'unread' => 'ungelesen',
'bookmark' => 'lesezeichen',
'remove bookmark' => 'lesezeichen entfernen',
@@ -230,4 +222,7 @@ return array(
// 'Remove this feed' => '',
// 'Miniflux' => '',
// 'miniflux' => '',
+ // 'Username:' => '',
+ // 'Password:' => '',
+ // 'All' => '',
);
diff --git a/locales/es_ES/translations.php b/locales/es_ES/translations.php
index 0bf0815..2d6ef9f 100644
--- a/locales/es_ES/translations.php
+++ b/locales/es_ES/translations.php
@@ -96,14 +96,6 @@ return array(
'Never' => 'Nunca',
'After %d day' => 'Después de %d día',
'After %d days' => 'Después de %d días',
- 'French' => 'Francés',
- 'English' => 'Inglés',
- 'German' => 'Alemán',
- 'Italian' => 'Italiano',
- 'Spanish' => 'Español',
- 'Simplified Chinese' => 'Chino simplificado',
- 'Czech' => 'Checo',
- 'Portuguese' => 'Portugués',
'unread' => 'no leídos',
'bookmark' => 'añadir a marcadores',
'remove bookmark' => 'borrar marcador',
@@ -230,4 +222,7 @@ return array(
// 'Remove this feed' => '',
// 'Miniflux' => '',
// 'miniflux' => '',
+ // 'Username:' => '',
+ // 'Password:' => '',
+ // 'All' => '',
);
diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php
index 887471d..15f9c03 100644
--- a/locales/fr_FR/translations.php
+++ b/locales/fr_FR/translations.php
@@ -96,14 +96,6 @@ return array(
'Never' => 'Jamais',
'After %d day' => 'Après %d jour',
'After %d days' => 'Après %d jours',
- 'French' => 'Français',
- 'English' => 'Anglais',
- 'German' => 'Allemand',
- 'Italian' => 'Italien',
- 'Spanish' => 'Espagnol',
- 'Simplified Chinese' => 'Chinois simplifié',
- 'Czech' => 'Tchèque',
- 'Portuguese' => 'Portuguais',
'unread' => 'non lus',
'bookmark' => 'ajouter aux favoris',
'remove bookmark' => 'supprimer des favoris',
@@ -230,4 +222,7 @@ return array(
'Remove this feed' => 'Supprimer cet abonnement',
'Miniflux' => 'Miniflux',
'miniflux' => 'miniflux',
+ 'Username:' => 'Utilisateur :',
+ 'Password:' => 'Mot de passe :',
+ 'All' => 'Tout',
);
diff --git a/locales/it_IT/translations.php b/locales/it_IT/translations.php
index 859e9c8..964ddc1 100644
--- a/locales/it_IT/translations.php
+++ b/locales/it_IT/translations.php
@@ -96,14 +96,6 @@ return array(
'Never' => 'Mai',
'After %d day' => 'Dopo %d giorno',
'After %d days' => 'Dopo %d giorni',
- 'French' => 'Francese',
- 'English' => 'Inglese',
- 'German' => 'Tedesco',
- 'Italian' => 'Italiano',
- // 'Spanish' => '',
- 'Simplified Chinese' => 'Cinese semplificato',
- 'Czech' => 'Ceco',
- // 'Portuguese' => '',
'unread' => 'non letti',
'bookmark' => 'bookmark',
'remove bookmark' => 'cancella bookmark',
@@ -230,4 +222,7 @@ return array(
// 'Remove this feed' => '',
// 'Miniflux' => '',
// 'miniflux' => '',
+ // 'Username:' => '',
+ // 'Password:' => '',
+ // 'All' => '',
);
diff --git a/locales/pt_BR/translations.php b/locales/pt_BR/translations.php
index 8ec40c5..062967d 100644
--- a/locales/pt_BR/translations.php
+++ b/locales/pt_BR/translations.php
@@ -96,14 +96,6 @@ return array(
'Never' => 'Nunca',
'After %d day' => 'Depois %d dias',
'After %d days' => 'Depois %d dias',
- 'French' => 'Frances',
- 'English' => 'Ingles',
- 'German' => 'Alemão',
- 'Italian' => 'Italiano',
- // 'Spanish' => '',
- 'Simplified Chinese' => 'Chinês Simplificado',
- // 'Czech' => '',
- 'Portuguese' => 'Português',
'unread' => 'não lido',
'bookmark' => 'lesezeichen',
'remove bookmark' => 'lesezeichen löschen',
@@ -230,4 +222,7 @@ return array(
// 'Remove this feed' => '',
// 'Miniflux' => '',
// 'miniflux' => '',
+ // 'Username:' => '',
+ // 'Password:' => '',
+ // 'All' => '',
);
diff --git a/locales/zh_CN/translations.php b/locales/zh_CN/translations.php
index 01b199a..66cd4f4 100644
--- a/locales/zh_CN/translations.php
+++ b/locales/zh_CN/translations.php
@@ -96,14 +96,6 @@ return array(
'Never' => '从不',
'After %d day' => '%d 天之后',
'After %d days' => '%d 天之后',
- 'French' => '法语',
- 'English' => '英语',
- 'German' => '德语',
- 'Italian' => '意大利人',
- // 'Spanish' => '',
- 'Simplified Chinese' => '简体中文',
- 'Czech' => '捷克语',
- // 'Portuguese' => '',
'unread' => '未读',
'bookmark' => '收藏',
'remove bookmark' => '取消收藏',
@@ -230,4 +222,7 @@ return array(
// 'Remove this feed' => '',
// 'Miniflux' => '',
// 'miniflux' => '',
+ // 'Username:' => '',
+ // 'Password:' => '',
+ // 'All' => '',
);
diff --git a/models/config.php b/models/config.php
index bf22278..00ecb19 100644
--- a/models/config.php
+++ b/models/config.php
@@ -2,13 +2,14 @@
namespace Model\Config;
+use DirectoryIterator;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PicoDb\Database;
use PicoFeed\Config as ReaderConfig;
use PicoFeed\Logging;
-const DB_VERSION = 28;
+const DB_VERSION = 29;
const HTTP_USER_AGENT = 'Miniflux (http://miniflux.net)';
// Get PicoFeed config
@@ -34,7 +35,6 @@ function get_reader_config()
function get_iframe_whitelist()
{
return array(
- '//www.youtube.com',
'http://www.youtube.com',
'https://www.youtube.com',
'http://player.vimeo.com',
@@ -62,27 +62,23 @@ function write_debug()
// Get available timezone
function get_timezones()
{
- $timezones = \timezone_identifiers_list();
+ $timezones = timezone_identifiers_list();
return array_combine(array_values($timezones), $timezones);
}
// Get all supported languages
function get_languages()
{
- $languages = array(
- 'cs_CZ' => t('Czech'),
- 'de_DE' => t('German'),
- 'en_US' => t('English'),
- 'es_ES' => t('Spanish'),
- 'fr_FR' => t('French'),
- 'it_IT' => t('Italian'),
- 'pt_BR' => t('Portuguese'),
- 'zh_CN' => t('Simplified Chinese'),
+ return array(
+ 'cs_CZ' => 'Čeština',
+ 'de_DE' => 'Deutsch',
+ 'en_US' => 'English',
+ 'es_ES' => 'Español',
+ 'fr_FR' => 'Français',
+ 'it_IT' => 'Italiano',
+ 'pt_BR' => 'Português',
+ 'zh_CN' => '简体中国',
);
-
- asort($languages);
-
- return $languages;
}
// Get all skins
@@ -94,7 +90,7 @@ function get_themes()
if (file_exists(THEME_DIRECTORY)) {
- $dir = new \DirectoryIterator(THEME_DIRECTORY);
+ $dir = new DirectoryIterator(THEME_DIRECTORY);
foreach ($dir as $fileinfo) {
@@ -180,6 +176,7 @@ function new_tokens()
'api_token' => generate_token(),
'feed_token' => generate_token(),
'bookmarklet_token' => generate_token(),
+ 'fever_token' => substr(generate_token(), 0, 8),
);
return Database::get('db')->table('config')->update($values);
@@ -242,6 +239,7 @@ function get_all()
'theme',
'api_token',
'feed_token',
+ 'fever_token',
'bookmarklet_token',
'auth_google_token',
'auth_mozilla_token',
diff --git a/models/schema.php b/models/schema.php
index d4a492d..9bf297d 100644
--- a/models/schema.php
+++ b/models/schema.php
@@ -2,6 +2,14 @@
namespace Schema;
+use PDO;
+use Model\Config;
+
+function version_29($pdo)
+{
+ $pdo->exec('ALTER TABLE config ADD COLUMN fever_token INTEGER DEFAULT "'.substr(Config\generate_token(), 0, 8).'"');
+}
+
function version_28($pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN rtl INTEGER DEFAULT 0');
@@ -14,7 +22,7 @@ function version_27($pdo)
function version_26($pdo)
{
- $pdo->exec('ALTER TABLE config ADD COLUMN bookmarklet_token TEXT DEFAULT "'.\Model\Config\generate_token().'"');
+ $pdo->exec('ALTER TABLE config ADD COLUMN bookmarklet_token TEXT DEFAULT "'.Config\generate_token().'"');
}
function version_25($pdo)
@@ -95,7 +103,7 @@ function version_15($pdo)
function version_14($pdo)
{
- $pdo->exec('ALTER TABLE config ADD COLUMN feed_token TEXT DEFAULT "'.\Model\Config\generate_token().'"');
+ $pdo->exec('ALTER TABLE config ADD COLUMN feed_token TEXT DEFAULT "'.Config\generate_token().'"');
}
function version_13($pdo)
@@ -105,7 +113,7 @@ function version_13($pdo)
function version_12($pdo)
{
- $pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.\Model\Config\generate_token().'"');
+ $pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.Config\generate_token().'"');
}
function version_11($pdo)
@@ -119,7 +127,7 @@ function version_11($pdo)
$rq->execute();
- $items = $rq->fetchAll(\PDO::FETCH_ASSOC);
+ $items = $rq->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
diff --git a/templates/config.php b/templates/config.php
index 7dfb12d..52bac8a 100644
--- a/templates/config.php
+++ b/templates/config.php
@@ -72,15 +72,27 @@
-
= t('API') ?>
+
= t('Fever API') ?>
+
+ - = t('Link:') ?> = Helper\get_current_base_url().'fever/' ?>
+ - = t('Username:') ?> = Helper\escape($values['username']) ?>
+ - = t('Password:') ?> = Helper\escape($values['fever_token']) ?>
+
+
+
+
+
= t('API') ?>
+
- = t('API endpoint:') ?> = Helper\get_current_base_url().'jsonrpc.php' ?>
- = t('API username:') ?> = Helper\escape($values['username']) ?>
- = t('API token:') ?> = Helper\escape($values['api_token']) ?>