First version of the JsonRPC API

This commit is contained in:
Frederic Guillot 2013-07-28 15:44:51 -04:00
parent 49930f2144
commit 6dcf1f9a81
9 changed files with 578 additions and 86 deletions

1
examples/.htaccess Normal file
View File

@ -0,0 +1 @@
Deny from all

40
examples/api_client.php Normal file
View File

@ -0,0 +1,40 @@
<?php
require '../vendor/JsonRPC/Client.php';
use JsonRPC\Client;
$client = new Client('http://webapps/miniflux/jsonrpc.php');
$client->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);

142
jsonrpc.php Normal file
View File

@ -0,0 +1,142 @@
<?php
require 'common.php';
require 'vendor/JsonRPC/Server.php';
use JsonRPC\Server;
$server = new Server;
$server->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();

View File

@ -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')),
));
}

View File

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

View File

@ -37,10 +37,18 @@
<h2><?= t('More informations') ?></h2>
</div>
<section>
<div class="alert alert-normal">
<h3><?= t('API') ?></h3>
<ul>
<li><?= t('API endpoint:') ?> <strong><?= Helper\get_current_base_url().'jsonrpc.php' ?></strong></li>
<li><?= t('API username:') ?> <strong><?= Helper\escape($values['username']) ?></strong></li>
<li><?= t('API token:') ?> <strong><?= Helper\escape($values['api_token']) ?></strong></li>
</ul>
</div>
<div class="alert alert-normal">
<h3><?= t('Database') ?></h3>
<ul>
<li><?= t('Database size:') ?> <?= Helper\format_bytes($db_size) ?></li>
<li><?= t('Database size:') ?> <strong><?= Helper\format_bytes($db_size) ?></strong></li>
<li><a href="?action=optimize-db"><?= t('Optimize the database') ?></a> <?= t('(VACUUM command)') ?></li>
<li><a href="?action=download-db"><?= t('Download the entire database') ?></a> <?= t('(Gzip compressed Sqlite file)') ?></li>
</ul>

92
vendor/JsonRPC/Client.php vendored Normal file
View File

@ -0,0 +1,92 @@
<?php
namespace JsonRPC;
class Client
{
private $url;
private $timeout;
private $debug;
private $username;
private $password;
private $headers = array(
'Connection: close',
'Content-Type: application/json',
'Accept: application/json'
);
public function __construct($url, $timeout = 5, $debug = false, $headers = array())
{
$this->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();
}
}

227
vendor/JsonRPC/Server.php vendored Normal file
View File

@ -0,0 +1,227 @@
<?php
namespace JsonRPC;
class Server
{
private $payload;
static private $procedures = array();
public function __construct($payload = '')
{
$this->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);
}
}

View File

@ -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 .= '<ul class="form-errors">';
foreach ($errors[$name] as $error) {
$html .= '<li>'.escape($error).'</li>';
}
@ -117,32 +99,26 @@ function error_list(array $errors, $name)
return $html;
}
function form_value($values, $name)
{
if (isset($values->$name)) {
return 'value="'.escape($values->$name).'"';
}
return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : '';
}
function form_hidden($name, $values = array())
{
return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).'/>';
}
function form_default_select($name, array $options, $values = array(), array $errors = array(), $class = '')
{
$options = array('' => '?') + $options;
return form_select($name, $options, $values, $errors, $class);
}
function form_select($name, array $options, $values = array(), array $errors = array(), $class = '')
{
$html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'">';
@ -163,74 +139,32 @@ function form_select($name, array $options, $values = array(), array $errors = a
return $html;
}
function form_radios($name, array $options, array $values = array())
{
$html = '';
foreach ($options as $value => $label) {
$html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value);
}
return $html;
}
function form_radio($name, $label, $value, $selected = false, $class = '')
{
return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($selected ? 'selected="selected"' : '').'>'.escape($label).'</label>';
}
function form_checkbox($name, $label, $value, $checked = false, $class = '')
{
return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($checked ? 'checked="checked"' : '').'>'.escape($label).'</label>';
}
function form_label($label, $name, $class = '')
{
return '<label for="form-'.$name.'" class="'.$class.'">'.escape($label).'</label>';
}
function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
$html = '<input type="text" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
$html .= implode(' ', $attributes).'/>';
$html .= error_list($errors, $name);
return $html;
}
function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
$html = '<input type="password" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
$html .= implode(' ', $attributes).'/>';
$html .= error_list($errors, $name);
return $html;
}
function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
$html = '<input type="email" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
$html .= implode(' ', $attributes).'/>';
$html .= error_list($errors, $name);
return $html;
}
function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
@ -242,4 +176,35 @@ function form_textarea($name, $values = array(), array $errors = array(), array
$html .= error_list($errors, $name);
return $html;
}
function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
$html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
$html .= implode(' ', $attributes).'/>';
$html .= error_list($errors, $name);
return $html;
}
function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return form_input('text', $name, $values, $errors, $attributes, $class);
}
function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return form_input('password', $name, $values, $errors, $attributes, $class);
}
function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return form_input('email', $name, $values, $errors, $attributes, $class);
}
function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return form_input('date', $name, $values, $errors, $attributes, $class);
}