From 6dcf1f9a812762f11457bcc232d0b9ac77483193 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 28 Jul 2013 15:44:51 -0400 Subject: [PATCH] First version of the JsonRPC API --- examples/.htaccess | 1 + examples/api_client.php | 40 +++++++ jsonrpc.php | 142 ++++++++++++++++++++++ model.php | 33 ++++-- schema.php | 6 + templates/config.php | 10 +- vendor/JsonRPC/Client.php | 92 +++++++++++++++ vendor/JsonRPC/Server.php | 227 ++++++++++++++++++++++++++++++++++++ vendor/PicoTools/Helper.php | 113 +++++++----------- 9 files changed, 578 insertions(+), 86 deletions(-) create mode 100644 examples/.htaccess create mode 100644 examples/api_client.php create mode 100644 jsonrpc.php create mode 100644 vendor/JsonRPC/Client.php create mode 100644 vendor/JsonRPC/Server.php 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 @@

+
+

+
    +
  • +
  • +
  • +
+

    -
  • +
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 .= '
    '; foreach ($errors[$name] as $error) { - $html .= '
  • '.escape($error).'
  • '; } @@ -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 ''; } - 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 = ''.escape($label).''; } - function form_checkbox($name, $label, $value, $checked = false, $class = '') { return ''; } - function form_label($label, $name, $class = '') { return ''; } - -function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - $class .= error_class($errors, $name); - - $html = ''; - $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 = ''; - $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 = ''; - $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 = ''; + $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); } \ No newline at end of file