df4f3b2a52
Utilize apaches mod_rewrite to append a database parameter to the fever api script based on the requested URL. Basically everything after /fever/ is treated as the desired database. E.g. using https://www.miniflux.net/fever/custom.sqlite/ as server url selects this database as sync source/target. The user supplied database parameter is compared with the output of Model\Database\get_all(), which ensures that only files within the DATA_DIRECTORY can be referenced (prevents File Inclusion/Directory Traversal vulnerabilities). I've tested it against Reeder on iOS. Note that Reeder does not allow the same user name to be used multiple times within the same domain name, even if the used URLs are different.
313 lines
7.3 KiB
PHP
313 lines
7.3 KiB
PHP
<?php
|
|
|
|
require '../common.php';
|
|
|
|
use Model\Feed;
|
|
use PicoDb\Database;
|
|
|
|
// Route handler
|
|
function route($name, Closure $callback = null)
|
|
{
|
|
static $routes = array();
|
|
|
|
if ($callback !== null) {
|
|
$routes[$name] = $callback;
|
|
}
|
|
else if (isset($routes[$name])) {
|
|
$routes[$name]();
|
|
}
|
|
}
|
|
|
|
// Serialize the payload in Json (XML is not supported)
|
|
function response(array $response)
|
|
{
|
|
header('Content-Type: application/json');
|
|
echo json_encode($response);
|
|
exit;
|
|
}
|
|
|
|
// Fever authentication
|
|
function auth()
|
|
{
|
|
if (!empty($_GET['database'])) {
|
|
Model\Database\select($_GET['database']);
|
|
}
|
|
|
|
$credentials = Database::get('db')->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']) {
|
|
|
|
$query = Database::get('db')
|
|
->table('items')
|
|
->limit(50)
|
|
->columns(
|
|
'rowid',
|
|
'feed_id',
|
|
'title',
|
|
'author',
|
|
'content',
|
|
'url',
|
|
'updated',
|
|
'status',
|
|
'bookmark'
|
|
);
|
|
|
|
if (isset($_GET['since_id']) && is_numeric($_GET['since_id'])) {
|
|
|
|
$items = $query->gt('rowid', $_GET['since_id'])
|
|
->asc('rowid');
|
|
}
|
|
else 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'])
|
|
&& ! is_null(filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT, array('options' => array('default' => NULL,'min_range' => -1)))) ){
|
|
|
|
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());
|