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

+

+ +
+
+

+
+
+

+