diff --git a/examples/.htaccess b/examples/.htaccess
new file mode 100644
index 0000000..14249c5
--- /dev/null
+++ b/examples/.htaccess
@@ -0,0 +1 @@
+Deny from all
\ No newline at end of file
diff --git a/examples/api_client.php b/examples/api_client.php
new file mode 100644
index 0000000..9f3e0cd
--- /dev/null
+++ b/examples/api_client.php
@@ -0,0 +1,40 @@
+authentication('admin', 'Nwjt73kOhC0K2mF');
+
+$result = $client->execute('feed.create', array('url' => 'http://bbc.co.uk/news'));
+var_dump($result);
+
+$result = $client->execute('feed.list');
+print_r($result);
+
+$feed_id = $result[0]['id'];
+
+$result = $client->execute('feed.update', array('feed_id' => $feed_id));
+var_dump($result);
+
+$result = $client->execute('feed.info', array('feed_id' => $feed_id));
+print_r($result);
+
+$result = $client->execute('feed.delete', array('feed_id' => $feed_id));
+var_dump($result);
+
+//$result = $client->execute('item.list_unread');
+//print_r($result);
+
+$result = $client->execute('item.list_unread', array('offset' => 5, 'limit' => 2));
+print_r($result);
+
+if (count($result)) {
+
+ $result = $client->execute('item.bookmark.create', array('item_id' => $result[0]['id']));
+ var_dump($result);
+}
+
+$result = $client->execute('item.bookmark.list');
+print_r($result);
\ No newline at end of file
diff --git a/jsonrpc.php b/jsonrpc.php
new file mode 100644
index 0000000..debb787
--- /dev/null
+++ b/jsonrpc.php
@@ -0,0 +1,142 @@
+authentication(array(
+ Model\get_config_value('username') => Model\get_config_value('api_token')
+));
+
+// Get all feeds
+$server->register('feed.list', function () {
+
+ return Model\get_feeds();
+});
+
+// Get one feed
+$server->register('feed.info', function ($feed_id) {
+
+ return Model\get_feed($feed_id);
+});
+
+// Add a new feed
+$server->register('feed.create', function($url) {
+
+ $result = Model\import_feed($url);
+ Model\write_debug();
+
+ return $result;
+});
+
+// Delete a feed
+$server->register('feed.delete', function($feed_id) {
+
+ return Model\remove_feed($feed_id);
+});
+
+// Update a feed
+$server->register('feed.update', function($feed_id) {
+
+ return Model\update_feed($feed_id);
+});
+
+// Get all items for a specific feed
+$server->register('item.feed.list', function ($feed_id, $offset = null, $limit = null) {
+
+ return Model\get_feed_items($feed_id, $offset, $limit);
+});
+
+// Count all feed items
+$server->register('item.feed.count', function ($feed_id) {
+
+ return Model\count_feed_items($feed_id);
+});
+
+// Get all bookmark items
+$server->register('item.bookmark.list', function ($offset = null, $limit = null) {
+
+ return Model\get_bookmarks($offset, $limit);
+});
+
+// Count bookmarks
+$server->register('item.bookmark.count', function () {
+
+ return Model\count_bookmarks();
+});
+
+// Add a bookmark
+$server->register('item.bookmark.create', function ($item_id) {
+
+ return Model\set_bookmark_value($item_id, 1);
+});
+
+// Remove a bookmark
+$server->register('item.bookmark.remove', function ($item_id) {
+
+ return Model\set_bookmark_value($item_id, 0);
+});
+
+// Get all unread items
+$server->register('item.list_unread', function ($offset = null, $limit = null) {
+
+ return Model\get_unread_items($offset, $limit);
+});
+
+// Count all unread items
+$server->register('item.count_unread', function () {
+
+ return Model\count_items('unread');
+});
+
+// Get all read items
+$server->register('item.list_read', function ($offset = null, $limit = null) {
+
+ return Model\get_read_items($offset, $limit);
+});
+
+// Count all read items
+$server->register('item.count_read', function () {
+
+ return Model\count_items('read');
+});
+
+// Get one item
+$server->register('item.info', function ($item_id) {
+
+ return Model\get_item($item_id);
+});
+
+// Delete an item
+$server->register('item.delete', function($item_id) {
+
+ return Model\set_item_removed($item_id);
+});
+
+// Mark item as read
+$server->register('item.mark_as_read', function($item_id) {
+
+ return Model\set_item_read($item_id);
+});
+
+// Mark item as unread
+$server->register('item.mark_as_read', function($item_id) {
+
+ return Model\set_item_unread($item_id);
+});
+
+// Flush all read items
+$server->register('item.flush', function() {
+
+ return Model\mark_as_removed();
+});
+
+// Mark all unread items as read
+$server->register('item.mark_all_as_read', function() {
+
+ return Model\mark_as_read();
+});
+
+echo $server->execute();
\ No newline at end of file
diff --git a/model.php b/model.php
index 7e3d97a..580aa95 100644
--- a/model.php
+++ b/model.php
@@ -22,7 +22,7 @@ use PicoFeed\Reader;
use PicoFeed\Export;
-const DB_VERSION = 11;
+const DB_VERSION = 12;
const HTTP_USERAGENT = 'Miniflux - http://miniflux.net';
const LIMIT_ALL = -1;
@@ -99,6 +99,12 @@ function write_debug()
}
+function generate_api_token()
+{
+ return substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
+}
+
+
function export_feeds()
{
$opml = new Export(get_feeds());
@@ -167,10 +173,10 @@ function import_feed($url)
$feed_id = $db->getConnection()->getLastId();
update_items($feed_id, $feed->items);
+
+ return (int) $feed_id;
}
}
-
- return true;
}
return false;
@@ -193,6 +199,7 @@ function update_feeds($limit = LIMIT_ALL)
function update_feed($feed_id)
{
$feed = get_feed($feed_id);
+ if (empty($feed)) return false;
$reader = new Reader;
@@ -438,7 +445,7 @@ function get_nav_item($item)
function set_item_removed($id)
{
- \PicoTools\singleton('db')
+ return \PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'removed', 'content' => ''));
@@ -447,7 +454,7 @@ function set_item_removed($id)
function set_item_read($id)
{
- \PicoTools\singleton('db')
+ return \PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'read'));
@@ -456,7 +463,7 @@ function set_item_read($id)
function set_item_unread($id)
{
- \PicoTools\singleton('db')
+ return \PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'unread'));
@@ -465,7 +472,7 @@ function set_item_unread($id)
function set_bookmark_value($id, $value)
{
- \PicoTools\singleton('db')
+ return \PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('bookmark' => $value));
@@ -506,7 +513,7 @@ function switch_item_status($id)
// Mark all items as read
function mark_as_read()
{
- \PicoTools\singleton('db')
+ return \PicoTools\singleton('db')
->table('items')
->eq('status', 'unread')
->save(array('status' => 'read'));
@@ -528,7 +535,7 @@ function mark_items_as_read(array $items_id)
function mark_as_removed()
{
- \PicoTools\singleton('db')
+ return \PicoTools\singleton('db')
->table('items')
->eq('status', 'read')
->eq('bookmark', 0)
@@ -646,7 +653,7 @@ function get_config()
{
return \PicoTools\singleton('db')
->table('config')
- ->columns('username', 'language', 'autoflush', 'nocontent', 'items_per_page', 'theme')
+ ->columns('username', 'language', 'autoflush', 'nocontent', 'items_per_page', 'theme', 'api_token')
->findOne();
}
@@ -718,7 +725,11 @@ function validate_config_update(array $values)
$v = new Validator($values, array(
new Validators\Required('username', t('The user name is required')),
- new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50)
+ new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
+ new Validators\Required('autoflush', t('Value required')),
+ new Validators\Required('items_per_page', t('Value required')),
+ new Validators\Integer('items_per_page', t('Must be an integer')),
+ new Validators\Required('theme', t('Value required')),
));
}
diff --git a/schema.php b/schema.php
index cc764be..247c94c 100644
--- a/schema.php
+++ b/schema.php
@@ -3,6 +3,12 @@
namespace Schema;
+function version_12($pdo)
+{
+ $pdo->exec('ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT "'.\Model\generate_api_token().'"');
+}
+
+
function version_11($pdo)
{
$rq = $pdo->prepare('
diff --git a/templates/config.php b/templates/config.php
index 6af5c9b..f7fb151 100644
--- a/templates/config.php
+++ b/templates/config.php
@@ -37,10 +37,18 @@
= t('More informations') ?>
+
+
= 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']) ?>
+
+
= t('Database') ?>
diff --git a/vendor/JsonRPC/Client.php b/vendor/JsonRPC/Client.php
new file mode 100644
index 0000000..4f90eea
--- /dev/null
+++ b/vendor/JsonRPC/Client.php
@@ -0,0 +1,92 @@
+url = $url;
+ $this->timeout = $timeout;
+ $this->debug = $debug;
+ $this->headers = array_merge($this->headers, $headers);
+ }
+
+
+ public function authentication($username, $password)
+ {
+ $this->username = $username;
+ $this->password = $password;
+ }
+
+
+ public function execute($procedure, array $params = array())
+ {
+ $id = mt_rand();
+
+ $payload = array(
+ 'jsonrpc' => '2.0',
+ 'method' => $procedure,
+ 'id' => $id
+ );
+
+ if (! empty($params)) {
+
+ $payload['params'] = $params;
+ }
+
+ $result = $this->doRequest($payload);
+
+ if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) {
+
+ return $result['result'];
+ }
+ else if ($this->debug && isset($result['error'])) {
+
+ print_r($result['error']);
+ }
+
+ return null;
+ }
+
+
+ public function doRequest($payload)
+ {
+ $ch = curl_init();
+
+ curl_setopt($ch, CURLOPT_URL, $this->url);
+ curl_setopt($ch, CURLOPT_HEADER, false);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
+ curl_setopt($ch, CURLOPT_USERAGENT, 'JSON-RPC PHP Client');
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
+
+ if ($this->username && $this->password) {
+
+ curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password);
+ }
+
+ $result = curl_exec($ch);
+ $response = json_decode($result, true);
+
+ curl_close($ch);
+
+ return is_array($response) ? $response : array();
+ }
+}
diff --git a/vendor/JsonRPC/Server.php b/vendor/JsonRPC/Server.php
new file mode 100644
index 0000000..418924c
--- /dev/null
+++ b/vendor/JsonRPC/Server.php
@@ -0,0 +1,227 @@
+payload = $payload;
+ }
+
+
+ public function allowHosts(array $hosts) {
+
+ if (! in_array($_SERVER['REMOTE_ADDR'], $hosts)) {
+
+ header('Content-Type: application/json');
+ header('HTTP/1.0 403 Forbidden');
+ echo '["Access Forbidden"]';
+ exit;
+ }
+ }
+
+
+ public function authentication(array $users)
+ {
+ // OVH workaround
+ if (isset($_SERVER['REMOTE_USER'])) {
+
+ list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', base64_decode(substr($_SERVER['REMOTE_USER'], 6)));
+ }
+
+ if (! isset($_SERVER['PHP_AUTH_USER']) ||
+ ! isset($users[$_SERVER['PHP_AUTH_USER']]) ||
+ $users[$_SERVER['PHP_AUTH_USER']] !== $_SERVER['PHP_AUTH_PW']) {
+
+ header('WWW-Authenticate: Basic realm="JsonRPC"');
+ header('Content-Type: application/json');
+ header('HTTP/1.0 401 Unauthorized');
+ echo '["Authentication failed"]';
+ exit;
+ }
+ }
+
+
+ public function register($name, \Closure $callback)
+ {
+ self::$procedures[$name] = $callback;
+ }
+
+
+ public function unregister($name)
+ {
+ if (isset(self::$procedures[$name])) {
+
+ unset(self::$procedures[$name]);
+ }
+ }
+
+
+ public function unregisterAll()
+ {
+ self::$procedures = array();
+ }
+
+
+ public function getResponse(array $data, array $payload = array())
+ {
+ if (! array_key_exists('id', $payload)) {
+
+ return '';
+ }
+
+ $response = array(
+ 'jsonrpc' => '2.0',
+ 'id' => $payload['id']
+ );
+
+ $response = array_merge($response, $data);
+
+ header('Content-Type: application/json');
+ return json_encode($response);
+ }
+
+
+ public function mapParameters(array $request_params, array $method_params, array &$params)
+ {
+ // Positional parameters
+ if (array_keys($request_params) === range(0, count($request_params) - 1)) {
+
+ if (count($request_params) !== count($method_params)) return false;
+ $params = $request_params;
+
+ return true;
+ }
+
+ // Named parameters
+ foreach ($method_params as $p) {
+
+ $name = $p->getName();
+
+ if (isset($request_params[$name])) {
+
+ $params[$name] = $request_params[$name];
+ }
+ else {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ public function execute()
+ {
+ // Parse payload
+ if (empty($this->payload)) {
+
+ $this->payload = file_get_contents('php://input');
+ }
+
+ if (is_string($this->payload)) {
+
+ $this->payload = json_decode($this->payload, true);
+ }
+
+ // Check JSON format
+ if (! is_array($this->payload)) {
+
+ return $this->getResponse(array(
+ 'error' => array(
+ 'code' => -32700,
+ 'message' => 'Parse error'
+ )),
+ array('id' => null)
+ );
+ }
+
+ // Handle batch request
+ if (array_keys($this->payload) === range(0, count($this->payload) - 1)) {
+
+ $responses = array();
+
+ foreach ($this->payload as $payload) {
+
+ if (! is_array($payload)) {
+
+ $responses[] = $this->getResponse(array(
+ 'error' => array(
+ 'code' => -32600,
+ 'message' => 'Invalid Request'
+ )),
+ array('id' => null)
+ );
+ }
+ else {
+
+ $server = new Server($payload);
+ $response = $server->execute();
+
+ if ($response) $responses[] = $response;
+ }
+ }
+
+ return empty($responses) ? '' : '['.implode(',', $responses).']';
+ }
+
+ // Check JSON-RPC format
+ if (! isset($this->payload['jsonrpc']) ||
+ ! isset($this->payload['method']) ||
+ ! is_string($this->payload['method']) ||
+ $this->payload['jsonrpc'] !== '2.0' ||
+ (isset($this->payload['params']) && ! is_array($this->payload['params']))) {
+
+ return $this->getResponse(array(
+ 'error' => array(
+ 'code' => -32600,
+ 'message' => 'Invalid Request'
+ )),
+ array('id' => null)
+ );
+ }
+
+ // Procedure not found
+ if (! isset(self::$procedures[$this->payload['method']])) {
+
+ return $this->getResponse(array(
+ 'error' => array(
+ 'code' => -32601,
+ 'message' => 'Method not found'
+ )),
+ $this->payload
+ );
+ }
+
+ $callback = self::$procedures[$this->payload['method']];
+ $params = array();
+
+ $reflection = new \ReflectionFunction($callback);
+
+ if (isset($this->payload['params'])) {
+
+ $parameters = $reflection->getParameters();
+
+ if (! $this->mapParameters($this->payload['params'], $parameters, $params)) {
+
+ return $this->getResponse(array(
+ 'error' => array(
+ 'code' => -32602,
+ 'message' => 'Invalid params'
+ )),
+ $this->payload
+ );
+ }
+ }
+
+ $result = $reflection->invokeArgs($params);
+
+ return $this->getResponse(array('result' => $result), $this->payload);
+ }
+}
diff --git a/vendor/PicoTools/Helper.php b/vendor/PicoTools/Helper.php
index 69528dc..b8ccf05 100644
--- a/vendor/PicoTools/Helper.php
+++ b/vendor/PicoTools/Helper.php
@@ -2,25 +2,25 @@
namespace Helper;
+function get_current_base_url()
+{
+ $url = isset($_SERVER['HTTPS']) ? 'https://' : 'http://';
+ $url .= $_SERVER['SERVER_NAME'];
+ $url .= dirname($_SERVER['PHP_SELF']).'/';
+
+ return $url;
+}
function escape($value)
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
}
-
-/**
- * Get the flash message if there is something
- *
- * @param string $html HTML tags of the flash message parsed with sprintf
- * return string HTML tags with the message or empty string if nothing
- */
function flash($html)
{
$data = '';
if (isset($_SESSION['flash_message'])) {
-
$data = sprintf($html, escape($_SESSION['flash_message']));
unset($_SESSION['flash_message']);
}
@@ -28,19 +28,11 @@ function flash($html)
return $data;
}
-
-/**
- * Get the flash error message if there is something
- *
- * @param string $html HTML tags of the flash message parsed with sprintf
- * return string HTML tags with the message or empty string if nothing
- */
function flash_error($html)
{
$data = '';
if (isset($_SESSION['flash_error_message'])) {
-
$data = sprintf($html, escape($_SESSION['flash_error_message']));
unset($_SESSION['flash_error_message']);
}
@@ -48,7 +40,6 @@ function flash_error($html)
return $data;
}
-
function format_bytes($size, $precision = 2)
{
$base = log($size) / log(1024);
@@ -57,47 +48,39 @@ function format_bytes($size, $precision = 2)
return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)];
}
-
function get_host_from_url($url)
{
return escape(parse_url($url, PHP_URL_HOST));
}
-
function summary($value, $min_length = 5, $max_length = 120, $end = '[...]')
{
$length = strlen($value);
if ($length > $max_length) {
-
return substr($value, 0, strpos($value, ' ', $max_length)).' '.$end;
}
else if ($length < $min_length) {
-
return '';
}
return $value;
}
-
function in_list($id, array $listing)
{
if (isset($listing[$id])) {
-
return escape($listing[$id]);
}
return '?';
}
-
function error_class(array $errors, $name)
{
return ! isset($errors[$name]) ? '' : ' form-error';
}
-
function error_list(array $errors, $name)
{
$html = '';
@@ -107,7 +90,6 @@ function error_list(array $errors, $name)
$html .= '