From 5c4c7ada978bd18bb5d5c4eed41d27f8a09442e3 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Thu, 14 Jul 2011 16:26:44 +0300 Subject: [PATCH 01/12] Fixed PHP 5.3 issues --- JsonRpcServer.php | 8 +++++--- jsonrpc_server.info | 2 +- jsonrpc_server.module | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index 823c4b6..3abf576 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -49,7 +49,7 @@ public function handle() { } //If needed, check if parameters can be omitted - $arg_count = count($this->method['args']); + $arg_count = (isset($this->method['args'])) ? count($this->method['args']) : 0; if (!isset($this->params)) { for ($i=0; $i<$arg_count; $i++) { $arg = $this->method['#args'][$i]; @@ -147,7 +147,7 @@ public function handle() { } // We are returning JSON, so tell the browser. - drupal_set_header('Content-Type: application/json; charset=utf-8'); + drupal_add_http_header('Content-Type', 'application/json charset=utf-8'); // Services assumes parameter positions to match the method callback's // function signature so we need to sort arguments by position (key) @@ -156,7 +156,9 @@ public function handle() { // method definitions instead of requiring all parameters to be present, as // we do now. // For reference: http://drupal.org/node/715044 - ksort($this->args); + if (is_array($this->args)) { + ksort($this->args); + } //Call service method try { diff --git a/jsonrpc_server.info b/jsonrpc_server.info index f55fcd0..43800e1 100644 --- a/jsonrpc_server.info +++ b/jsonrpc_server.info @@ -3,4 +3,4 @@ name = "JSON-RPC Server" description = "Provides an JSON-RPC server." package = "Services - servers" dependencies[] = services -core = "6.x" +core = "7.x" diff --git a/jsonrpc_server.module b/jsonrpc_server.module index 09ea985..8c8a729 100644 --- a/jsonrpc_server.module +++ b/jsonrpc_server.module @@ -3,8 +3,8 @@ function jsonrpc_server_server_info() { return array( - '#name' => 'JSON-RPC', - '#path' => 'json-rpc', + 'name' => 'JSON-RPC', + 'path' => 'json-rpc', ); } From b427b3e7994778178bb35f7b4d436b62f62bdbb3 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Mon, 18 Jul 2011 16:40:06 +0000 Subject: [PATCH 02/12] Restore --- JsonRpcServer.php | 8 +++----- jsonrpc_server.info | 2 +- jsonrpc_server.module | 6 +++--- p.patch | 0 4 files changed, 7 insertions(+), 9 deletions(-) create mode 100644 p.patch diff --git a/JsonRpcServer.php b/JsonRpcServer.php index 3abf576..823c4b6 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -49,7 +49,7 @@ public function handle() { } //If needed, check if parameters can be omitted - $arg_count = (isset($this->method['args'])) ? count($this->method['args']) : 0; + $arg_count = count($this->method['args']); if (!isset($this->params)) { for ($i=0; $i<$arg_count; $i++) { $arg = $this->method['#args'][$i]; @@ -147,7 +147,7 @@ public function handle() { } // We are returning JSON, so tell the browser. - drupal_add_http_header('Content-Type', 'application/json charset=utf-8'); + drupal_set_header('Content-Type: application/json; charset=utf-8'); // Services assumes parameter positions to match the method callback's // function signature so we need to sort arguments by position (key) @@ -156,9 +156,7 @@ public function handle() { // method definitions instead of requiring all parameters to be present, as // we do now. // For reference: http://drupal.org/node/715044 - if (is_array($this->args)) { - ksort($this->args); - } + ksort($this->args); //Call service method try { diff --git a/jsonrpc_server.info b/jsonrpc_server.info index 43800e1..f55fcd0 100644 --- a/jsonrpc_server.info +++ b/jsonrpc_server.info @@ -3,4 +3,4 @@ name = "JSON-RPC Server" description = "Provides an JSON-RPC server." package = "Services - servers" dependencies[] = services -core = "7.x" +core = "6.x" diff --git a/jsonrpc_server.module b/jsonrpc_server.module index 8c8a729..beb29a1 100644 --- a/jsonrpc_server.module +++ b/jsonrpc_server.module @@ -3,8 +3,8 @@ function jsonrpc_server_server_info() { return array( - 'name' => 'JSON-RPC', - 'path' => 'json-rpc', + '#name' => 'JSON-RPC', + '#path' => 'json-rpc', ); } @@ -71,4 +71,4 @@ function jsonrpc_server_server() { function jsonrpc_server_add_javascript() { $path = drupal_get_path("module", "jsonrpc_server"); drupal_add_js($path ."/jsonrpc_server.js"); -} \ No newline at end of file +} diff --git a/p.patch b/p.patch new file mode 100644 index 0000000..e69de29 From dcd60e0bd1283f961ab08dda57930ba9f3a61121 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Mon, 18 Jul 2011 16:40:32 +0000 Subject: [PATCH 03/12] Restore --- p.patch | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 p.patch diff --git a/p.patch b/p.patch deleted file mode 100644 index e69de29..0000000 From 91c6d9a7c191a37eedbb1207da4cebbda24d6c2d Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Mon, 18 Jul 2011 16:42:45 +0000 Subject: [PATCH 04/12] #1004292 by cesarpo: Added Services 3 version. --- jsonrpc_server.module | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonrpc_server.module b/jsonrpc_server.module index 09ea985..5cb9544 100644 --- a/jsonrpc_server.module +++ b/jsonrpc_server.module @@ -3,8 +3,8 @@ function jsonrpc_server_server_info() { return array( - '#name' => 'JSON-RPC', - '#path' => 'json-rpc', + 'name' => 'JSON-RPC', + 'path' => 'json-rpc', ); } @@ -71,4 +71,4 @@ function jsonrpc_server_server() { function jsonrpc_server_add_javascript() { $path = drupal_get_path("module", "jsonrpc_server"); drupal_add_js($path ."/jsonrpc_server.js"); -} \ No newline at end of file +} From d9c0a0cb9f39c36405bbc03f78a4551c21e0f189 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Mon, 18 Jul 2011 16:53:38 +0000 Subject: [PATCH 05/12] Adapt to Drupal 7. --- JsonRpcServer.php | 2 +- jsonrpc_server.info | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index 823c4b6..bd6835e 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -147,7 +147,7 @@ public function handle() { } // We are returning JSON, so tell the browser. - drupal_set_header('Content-Type: application/json; charset=utf-8'); + drupal_add_http_header('Content-Type', 'application/json charset=utf-8'); // Services assumes parameter positions to match the method callback's // function signature so we need to sort arguments by position (key) diff --git a/jsonrpc_server.info b/jsonrpc_server.info index f55fcd0..43800e1 100644 --- a/jsonrpc_server.info +++ b/jsonrpc_server.info @@ -3,4 +3,4 @@ name = "JSON-RPC Server" description = "Provides an JSON-RPC server." package = "Services - servers" dependencies[] = services -core = "6.x" +core = "7.x" From e62b30c504014d8a05a2d7825ac0fd48ee48b88c Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Mon, 18 Jul 2011 17:05:06 +0000 Subject: [PATCH 06/12] Fix some PHP 5.3 warnings. --- JsonRpcServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index 823c4b6..6c93f11 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -45,7 +45,7 @@ public function handle() { if (!isset($this->method)) { // No method found is a fatal error $this->error(JSONRPC_ERROR_PROCEDURE_NOT_FOUND, t("Invalid method @method", - array('@method' => $request))); + array('@method' => $this->method_name))); } //If needed, check if parameters can be omitted From e5dd909330cb7f3a36d9f155d64d57b2d7e90e16 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Mon, 18 Jul 2011 17:11:12 +0000 Subject: [PATCH 07/12] Fix some PHP 5.3 warnings. --- JsonRpcServer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index 034343a..1ac3e37 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -28,7 +28,7 @@ public function __construct($in) { $this->id = isset($in['id']) ? $in['id'] : NULL; $this->version = isset($in['jsonrpc']) ? $in['jsonrpc'] : '1.1'; $this->major_version = intval(substr($this->version, 0, 1)); - $this->params = isset($in['params']) ? $in['params'] : NULL; + $this->params = isset($in['params']) ? $in['params'] : array(); } public function handle() { @@ -49,7 +49,7 @@ public function handle() { } //If needed, check if parameters can be omitted - $arg_count = count($this->method['args']); + $arg_count = $arg_count = (isset($this->method['args'])) ? count($this->method['args']) : 0; if (!isset($this->params)) { for ($i=0; $i<$arg_count; $i++) { $arg = $this->method['#args'][$i]; From cf82247dc125d09a459aa6e00f252ef6ff3bf45a Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Mon, 18 Jul 2011 17:41:51 +0000 Subject: [PATCH 08/12] Small fix in calculation. --- JsonRpcServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index 1ac3e37..f621db4 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -49,7 +49,7 @@ public function handle() { } //If needed, check if parameters can be omitted - $arg_count = $arg_count = (isset($this->method['args'])) ? count($this->method['args']) : 0; + $arg_count = isset($this->method['args']) ? count($this->method['args']) : 0; if (!isset($this->params)) { for ($i=0; $i<$arg_count; $i++) { $arg = $this->method['#args'][$i]; From 556319b824b05db1031d6467419465df4cda0d15 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Thu, 4 Aug 2011 13:29:09 +0000 Subject: [PATCH 09/12] Small correction on 'Content-Type' --- JsonRpcServer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index f621db4..cb360e7 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -147,7 +147,7 @@ public function handle() { } // We are returning JSON, so tell the browser. - drupal_add_http_header('Content-Type', 'application/json charset=utf-8'); + drupal_add_http_header('Content-Type', 'application/json; charset=utf-8'); // Services assumes parameter positions to match the method callback's // function signature so we need to sort arguments by position (key) @@ -235,4 +235,4 @@ public function response($response) { private function is_assoc($array) { return (is_array($array) && 0 !== count(array_diff_key($array, array_keys(array_keys($array))))); } -} \ No newline at end of file +} From e5fcdd0660da56a2f8c02bbb4ad7efe125b67928 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Sat, 6 Aug 2011 15:49:42 +0000 Subject: [PATCH 10/12] New PHP 5.3 notice. --- JsonRpcServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index cb360e7..65242c5 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -124,7 +124,7 @@ public function handle() { //Validate arguments for($i=0; $i<$arg_count; $i++) { - $val = $this->args[$i]; + $val = isset($this->args[$i]) ? $this->args[$i] : NULL; $arg = $this->method['args'][$i]; if (isset($val)) { //If we have data From 3c48680d063e71a5476312556ce67f7bc72ca3c0 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Sat, 20 Aug 2011 12:11:57 +0000 Subject: [PATCH 11/12] Respond with application code on error. See http://drupal.org/node/1255006. --- JsonRpcServer.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/JsonRpcServer.php b/JsonRpcServer.php index 65242c5..bf1d732 100644 --- a/JsonRpcServer.php +++ b/JsonRpcServer.php @@ -162,11 +162,16 @@ public function handle() { try { $result = services_controller_execute($this->method, $this->args); return $this->result($result); - } catch (ServicesException $e) { - $this->error(JSONRPC_ERROR_INTERNAL_ERROR, $e->getMessage(), $e->getData()); + } + catch (ServicesException $e) { + $application_error = $e->getCode(); + $error_code = empty($application_error) ? JSONRPC_ERROR_INTERNAL_ERROR : $application_error; + $this->error($error_code, $e->getMessage(), $e->getData()); } catch (Exception $e) { - $this->error(JSONRPC_ERROR_INTERNAL_ERROR, $e->getMessage()); + $application_error = $e->getCode(); + $error_code = empty($application_error) ? JSONRPC_ERROR_INTERNAL_ERROR : $application_error; + $this->error($error_code, $e->getMessage(), $e->getData()); } } From baf46269056edf539d164273dbbeb3ef6afdd467 Mon Sep 17 00:00:00 2001 From: Claudiu Cristea Date: Sat, 3 Sep 2011 14:37:09 +0300 Subject: [PATCH 12/12] Drupal 7 port. --- JsonRpcServer.php | 238 ------------------- jsonrpc_server.class.inc | 483 +++++++++++++++++++++++++++++++++++++++ jsonrpc_server.inc | 45 ++++ jsonrpc_server.info | 8 +- jsonrpc_server.js | 18 -- jsonrpc_server.module | 128 ++++++----- 6 files changed, 608 insertions(+), 312 deletions(-) delete mode 100644 JsonRpcServer.php create mode 100644 jsonrpc_server.class.inc create mode 100644 jsonrpc_server.inc delete mode 100644 jsonrpc_server.js diff --git a/JsonRpcServer.php b/JsonRpcServer.php deleted file mode 100644 index f621db4..0000000 --- a/JsonRpcServer.php +++ /dev/null @@ -1,238 +0,0 @@ -in = $in; - $this->method_name = isset($in['method']) ? $in['method'] : NULL; - $this->id = isset($in['id']) ? $in['id'] : NULL; - $this->version = isset($in['jsonrpc']) ? $in['jsonrpc'] : '1.1'; - $this->major_version = intval(substr($this->version, 0, 1)); - $this->params = isset($in['params']) ? $in['params'] : array(); - } - - public function handle() { - //A method is required, no matter what - if(empty($this->method_name)) { - $this->error(JSONRPC_ERROR_REQUEST, t("The received JSON not a valid JSON-RPC Request")); - } - - $endpoint = services_get_server_info('endpoint'); - - //Find the method - $this->method = services_controller_get($this->method_name, $endpoint); - $args = array(); - - if (!isset($this->method)) { // No method found is a fatal error - $this->error(JSONRPC_ERROR_PROCEDURE_NOT_FOUND, t("Invalid method @method", - array('@method' => $this->method_name))); - } - - //If needed, check if parameters can be omitted - $arg_count = isset($this->method['args']) ? count($this->method['args']) : 0; - if (!isset($this->params)) { - for ($i=0; $i<$arg_count; $i++) { - $arg = $this->method['#args'][$i]; - if (!$arg['optional']) { - if (empty($this->params)) { - // We have required parameter, but we don't have any. - if (is_array($this->params)) { - // The request has probably been parsed correctly if params is an array, - // just tell the client that we're missing parameters. - $this->error(JSONRPC_ERROR_PARAMS, t("No parameters received, the method '@method' has required parameters.", - array('@method'=>$this->method_name))); - } - else { - // If params isn't an array we probably have a syntax error in the json. - // Tell the client that there was a error while parsing the json. - // TODO: parse errors should be caught earlier - $this->error(JSONRPC_ERROR_PARSE, t("No parameters received, the likely reason is malformed json, the method '@method' has required parameters.", - array('@method'=>$this->method_name))); - } - } - } - } - } - - // Map parameters to arguments, the 1.1 draft is more generous than the 2.0 proposal when - // it comes to parameter passing. 1.1-d allows mixed positional and named parameters while - // 2.0-p forces the client to choose between the two. - // - // 2.0 proposal on parameters: http://groups.google.com/group/json-rpc/web/json-rpc-1-2-proposal#parameters-positional-and-named - // 1.1 draft on parameters: http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html#NamedPositionalParameters - if($this->array_is_assoc($this->params)) - { - $this->args = array(); - - //Create a assoc array to look up indexes for parameter names - $arg_dict = array(); - for ($i=0; $i<$arg_count; $i++) { - $arg = $this->method['args'][$i]; - $arg_dict[$arg['name']] = $i; - } - - foreach ($this->params as $key => $value) { - if ($this->major_version==1 && preg_match('/^\d+$/',$key)) { //A positional argument (only allowed in v1.1 calls) - if ($key >= $arg_count) { //Index outside bounds - $this->error(JSONRPC_ERROR_PARAMS, t("Positional parameter with a position outside the bounds (index: @index) received", - array('@index'=>$key))); - } - else { - $this->args[intval($key)] = $value; - } - } - else { //Associative key - if (!isset($arg_dict[$key])) { //Unknown parameter - $this->error(JSONRPC_ERROR_PARAMS, t("Unknown named parameter '@name' received", - array('@name'=>$key))); - } - else { - $this->args[$arg_dict[$key]] = $value; - } - } - } - } - else { //Non associative arrays can be mapped directly - $param_count = count($this->params); - if ($param_count > $arg_count) { - $this->error(JSONRPC_ERROR_PARAMS, t("Too many arguments received, the method '@method' only takes '@num' argument(s)", - array('@method'=>$this->method_name, '@num'=> $arg_count ))); - } - $this->args = $this->params; - } - - //Validate arguments - for($i=0; $i<$arg_count; $i++) - { - $val = $this->args[$i]; - $arg = $this->method['args'][$i]; - - if (isset($val)) { //If we have data - if ($arg['type'] == 'struct' && is_array($val) && $this->array_is_assoc($val)) { - $this->args[$i] = $val = (object)$val; - } - - //Only array-type parameters accepts arrays - if (is_array($val) && $arg['type']!='array' && !($this->is_assoc($val) && $arg['type'] == 'struct')){ - $this->error_wrong_type($arg, 'array'); - } - //Check that int and float value type arguments get numeric values - else if(($arg['type']=='int' || $arg['type']=='float') && !is_numeric($val)) { - $this->error_wrong_type($arg,'string'); - } - } - else if (!$arg['optional']) { //Trigger error if a required parameter is missing - $this->error(JSONRPC_ERROR_PARAMS, t("Argument '@name' is required but was not received", array('@name'=>$arg['name']))); - } - } - - // We are returning JSON, so tell the browser. - drupal_add_http_header('Content-Type', 'application/json charset=utf-8'); - - // Services assumes parameter positions to match the method callback's - // function signature so we need to sort arguments by position (key) - // before passing them to the method callback. The best solution here would - // be to pad optional parameters using a #default key in the hook_service - // method definitions instead of requiring all parameters to be present, as - // we do now. - // For reference: http://drupal.org/node/715044 - ksort($this->args); - - //Call service method - try { - $result = services_controller_execute($this->method, $this->args); - return $this->result($result); - } catch (ServicesException $e) { - $this->error(JSONRPC_ERROR_INTERNAL_ERROR, $e->getMessage(), $e->getData()); - } - catch (Exception $e) { - $this->error(JSONRPC_ERROR_INTERNAL_ERROR, $e->getMessage()); - } - } - - private function array_is_assoc(&$arr) { - $count = count($arr); - for ($i=0;$i<$count;$i++) { - if (!array_key_exists($i, $arr)) { - return true; - } - } - return false; - } - - private function response_version(&$response) { - switch ($this->major_version) { - case 2: - $response['jsonrpc'] = '2.0'; - break; - case 1: - $response['version'] = '1.1'; - break; - } - } - - private function response_id(&$response) { - if (!empty($this->id)) { - $response['id'] = $this->id; - } - } - - private function result($result) { - $response = array('result' => $result); - return $this->response($response); - } - - private function error($code, $message, $data = NULL) { - $response = array('error' => array('name' => 'JSONRPCError', 'code' => $code, 'message' => $message)); - if ($data) { - $response['data'] = $data; - } - throw new ServicesException($message, $code, $response); - } - - private function error_wrong_type(&$arg, $type){ - $this->error(JSONRPC_ERROR_PARAMS, t("The argument '@arg' should be a @type, not @used_type", - array( - '@arg' => $arg['name'], - '@type' => $arg['type'], - '@used_type' => $type, - ) - )); - } - - public function response($response) { - // Check if this is a 2.0 notification call - if($this->major_version==2 && empty($this->id)) - return; - - $this->response_version($response); - $this->response_id($response); - - //Using the current development version of Drupal 7:s drupal_to_js instead - return json_encode($response); - } - - private function is_assoc($array) { - return (is_array($array) && 0 !== count(array_diff_key($array, array_keys(array_keys($array))))); - } -} \ No newline at end of file diff --git a/jsonrpc_server.class.inc b/jsonrpc_server.class.inc new file mode 100644 index 0000000..e1c5f1f --- /dev/null +++ b/jsonrpc_server.class.inc @@ -0,0 +1,483 @@ +input = $input; + $this->httpMethod = $http_method; + $this->methodName = isset($input['method']) ? $input['method'] : NULL; + $this->id = isset($input['id']) ? $input['id'] : NULL; + $this->version = isset($input['jsonrpc']) ? $input['jsonrpc'] : '1.1'; + $this->majorVersion = intval(substr($this->version, 0, 1)); + $this->params = isset($input['params']) ? $input['params'] : array(); + $this->args = array(); + } + + /** + * Validate and prepare a JSON-RPC call according to specifications. + * + * Not all the validations a made here. + */ + protected function validate() { + + // A method is required. + if (empty($this->methodName)) { + $this->error(JSONRPC_ERROR_REQUEST, t('The received JSON not a valid JSON-RPC Request.')); + } + + // Check for if valid "params" were sent. + if (!is_array($this->params)) { + $this->error(JSONRPC_ERROR_PARSE, t('No valid parameters received. The "params" member of JSON must be an Array or Object.')); + } + + $endpoint = services_get_server_info('endpoint'); + + // Find the method definition as it was provided by the Services module. + $this->method = services_controller_get($this->methodName, $endpoint); + + // Check id this method has been defined in hook_services_resources(). + if (!isset($this->method)) { + $this->error(JSONRPC_ERROR_PROCEDURE_NOT_FOUND, t('Invalid method "@method".', array('@method' => $this->methodName))); + } + $this->method['args'] = isset($this->method['args']) ? $this->method['args'] : array(); + + $count_params = count($this->params); + $this->methodArgumentsCount = count($this->method['args']); + + // Too many parameters passed. + if ($count_params > $this->methodArgumentsCount) { + if ($this->methodArgumentsCount == 0) { + $this->error(JSONRPC_ERROR_PARAMS, t('@params parameters were passed but this method (@method) accepts none.', array('@params' => $count_params, '@method' => $this->methodName))); + } + elseif ($this->methodArgumentsCount == 1) { + $this->error(JSONRPC_ERROR_PARAMS, t('@params parameters were passed but this method (@method) accepts only one.', array('@params' => $count_params, '@method' => $this->methodName))); + } + else { + $this->error(JSONRPC_ERROR_PARAMS, t('@params parameters were passed but this method (@method) accepts only @args.', array('@params' => $count_params, '@args' => $this->methodArgumentsCount, '@method' => $this->methodName))); + } + } + + // JSON-RPC 2.0 doesn't allow mixture of named and positional parameters. Let's check that. + if ($this->majorVersion == 2) { + foreach ($this->params as $param_key => $param_value) { + + // Get only the first param type. + if (!isset($param_type)) { + $param_type = is_numeric($param_key) ? 0 : 1; + + // Just iterate after the first step. + continue; + } + + // Found parameter mixture? Break with an error. + if ((is_numeric($param_key) && $param_type == 1) || (is_string($param_key) && $param_type == 0)) { + $this->error(JSONRPC_ERROR_PARAMS, t("JSON-RPC version @ver doesn't alow mixture of named and positional parameters.", array('@ver' => $this->version))); + } + } + } + + // Build a list of argument names. + $args = array(); + foreach ($this->method['args'] as $arg) { + $args[] = $arg['name']; + } + + foreach ($this->params as $param_key => $param_value) { + // Check for positional parameters out-of-range. + if (is_numeric($param_key) && ($param_key >= $this->methodArgumentsCount)) { + $this->error(JSONRPC_ERROR_PARAMS, t('The index @position of the positional parameter @value is outside arguments range. Maximum allowed index @max.', array('@position' => $param_key, '@value' => drupal_json_encode($param_value), '@max' => ($this->methodArgumentsCount - 1)))); + } + // And for invalid names of named parameters. + elseif (is_string($param_key) && !in_array($param_key, $args)) { + $this->error(JSONRPC_ERROR_PARAMS, t('Unknown named parameter "@name" received.', array('@name' => $param_key))); + } + } + } + + /** + * Validate a single passed parameter against the method argument definition. + * + * @param Integer $delta + * The position in the method argument list. + * @param Array $arg + * The method argument definition. + * @param Arbitrary Variable $param + */ + protected function validateParam($delta, array $arg, &$param) { + + // Check if the argument is mandatory but no parameter has been passed. + if (!$arg['optional'] && empty($param)) { + $this->error(JSONRPC_ERROR_PARAMS, t('The argument "@arg" (index @delta) is required by the "@method" method but was not received.', array('@arg' => $arg['name'], '@delta' => $delta, '@method' => $this->methodName))); + } + + /** + * If "struct" is expected, convert associative array to object but only if + * the associative array has no numeric (positional) keys. Forcing those to + * object will remove them. + * + * Example: (object) array('somekey' => 'value', 2 => 'value2') results in + * {"somekey": "value"}. The numeric key item get lost. + */ + if (is_array($param) && ($arg['type'] == 'struct')) { + if (!((bool) count(array_filter(array_keys($param), 'is_numeric')))) { + $param = (object) $param; + } + + // We are leaving here. The parameter is either on Object, + // either an Array and a "struct" it's expected. + return; + } + + // Only array-type parameters accepts arrays. + if (is_array($param) && $arg['type'] != 'array') { + $this->errorWrongType($arg, 'array'); + } + + // Check that "int" or "float" value type arguments get numeric values. + if (in_array($arg['type'], array('int', 'float')) && !is_numeric($param)) { + $this->errorWrongType($arg, 'string'); + } + } + + /** + * Handle a JSON-RPC request and returns the response. + * + * @return + * A response or throw an ServicesException. + */ + public function handle() { + + // Validate & prepare. + $this->validate(); + + // Processing all sent parameters in the method argument order. + foreach ($this->method['args'] as $delta => $arg) { + + /** + * Check if there are 2 candidate parameters for the same argument. + * This may happen only in JSON-RPC 1.1 where a positional paramater may + * overlap a named parameter or viceversa. + */ + if (($this->majorVersion == 1) && isset($this->params[$delta]) && isset($this->params[$arg['name']])) { + $this->error(JSONRPC_ERROR_PARAMS, t('Two parameters "@delta": @positional and "@name": @named are disputing the same argument "@name".', array('@delta' => $delta, '@positional' => drupal_json_encode($this->params[$delta]), '@name' => $arg['name'], '@named' => drupal_json_encode($this->params[$arg['name']])))); + } + + // Find out the parameter for this argument. + $param = isset($this->params[$arg['name']]) ? $this->params[$arg['name']] : (isset($this->params[$delta]) ? $this->params[$delta] : NULL); + + // Parameter level validation. + $this->validateParam($delta, $arg, $param); + + // Optional argument. + if ($arg['optional']) { + + // No parameter sent for this argument. + if (is_null($param)) { + $this->args[] = isset($arg['default value']) ? $arg['default value'] : NULL; + } + // It's optional but a parameter has been sent. Use it. + else { + $this->args[] = $param; + } + } + // Mandatory argument. + else { + $this->args[] = $param; + } + } + + // Call the service method. + try { + + // Using the Services controller. + $result = services_controller_execute($this->method, $this->args); + return $this->result($result); + } + + // On error, expect a ServicesException first. + catch (ServicesException $e) { + + // Check first if the application has sent an error code. If yes, use it. + $application_error_code = $e->getCode(); + $error_code = empty($application_error_code) ? JSONRPC_ERROR_INTERNAL_ERROR : $application_error_code; + + $this->error($error_code, $e->getMessage(), $e->getData()); + } + + // Fallback to a common Exception. + catch (Exception $e) { + $this->error(JSONRPC_ERROR_INTERNAL_ERROR, $e->getMessage()); + } + } + + /** + * Respond to the JSON_RPC Request. + * + * @param Arbitrary Variable $result. + * + * @return String + * JSON encoded string with the service response. + */ + protected function result($result) { + $response = array('result' => $result); + + // Inform the browser that we are returning JSON. + drupal_add_http_header('Content-Type', 'application/json; charset=utf-8'); + + // Add other JSON-RPC protocol informations to the response and sent it. + return $this->buildResponse($response); + } + + /** + * Add additional informations to the response. + * + * The response must be according to JSON-RPC specifications. The main result + * will be wrapped together with other information. + * + * @param array $response + * An Associative array containing the basic response of the service. + * + * @return + * JSON encoded string with the complete response for this request. + */ + public function buildResponse($response) { + + /* + * In JSON-RPC 2.0 requests made without sending the Request ID ("id") are + * notifications. So we are still let the consumer that his request is OK by + * sending 200 but will not send any response. + * + * From version 2.0 specs: + * + * "A Notification is a special Request, without "id" and without Response. + * The server MUST NOT reply to a Notification." + * + * @todo Such request must receive a 204 HTTP Code. + */ + if ($this->majorVersion == 2 && empty($this->id)) { + return; + } + + // Add the version to the response. + $this->responseAddVersion($response); + + // Add the Request ID ("id") to the response. + $this->responseAddId($response); + + // Encode the response. + return drupal_json_encode($response); + } + + /** + * Add JSON-RPC Version to a response. + * + * @param array $response + * An Associative array containing the basic response of the service. + */ + protected function responseAddVersion(&$response) { + if ($this->majorVersion == 1) { + $response['version'] = '1.1'; + } + elseif ($this->majorVersion == 1) { + $response['jsonrpc'] = '2.0'; + } + } + + /** + * Add the Request ID ("id") to a response. + * + * @param array $response + * An Associative array containing the basic response of the service. + */ + protected function responseAddId(&$response) { + if (!empty($this->id)) { + $response['id'] = $this->id; + } + } + + /** + * General error processing function. + * + * @param Integer $code + * The error code. + * @param String $message + * The error message. + * @param Array $data + * An associative array with additional data to be passed. + */ + protected function error($code, $message, $data = NULL) { + $response = array('error' => array('name' => 'JSONRPCError', 'code' => $code, 'message' => $message)); + if ($data) { + $response['data'] = $data; + } + throw new ServicesException($message, $code, $response); + } + + /** + * Throw an wrong type error. + * + * @param Array $arg + * The method argument definition array. + * @param String $type + * The variable type to which this error is referring. + */ + protected function errorWrongType(array $arg, $type) { + $this->error(JSONRPC_ERROR_PARAMS, t('The argument "@arg" should have "@type" type, not "@used_type".', array('@arg' => $arg['name'], '@type' => $arg['type'], '@used_type' => $type))); + } +} diff --git a/jsonrpc_server.inc b/jsonrpc_server.inc new file mode 100644 index 0000000..016d598 --- /dev/null +++ b/jsonrpc_server.inc @@ -0,0 +1,45 @@ + 'checkbox', + '#title' => t('Allow GET requests'), + '#description' => t('Allow consumers to send GET requests against this endpoint.'), + '#default_value' => isset($settings['allow_get_requests']) ? $settings['allow_get_requests'] : FALSE, + ); +} + +/** + * Submit handler for the services JSON-RPC Server settings form. + * + * @param object $endpoint + * The endpoint that's being configured. + * @param array $values + * The partial form-state from services. + * + * @return array + * The settings for the REST server in this endpoint. + */ +function _jsonrpc_server_settings_submit($endpoint, &$values) { + return $values; +} diff --git a/jsonrpc_server.info b/jsonrpc_server.info index 43800e1..fd0c7cd 100644 --- a/jsonrpc_server.info +++ b/jsonrpc_server.info @@ -1,6 +1,8 @@ -; $Id$ name = "JSON-RPC Server" -description = "Provides an JSON-RPC server." +description = "Provides an JSON-RPC server for Services module." package = "Services - servers" dependencies[] = services -core = "7.x" +core = 7.x + +files[] = jsonrpc_server.module +files[] = jsonrpc_server.class.inc diff --git a/jsonrpc_server.js b/jsonrpc_server.js deleted file mode 100644 index 209c8db..0000000 --- a/jsonrpc_server.js +++ /dev/null @@ -1,18 +0,0 @@ -// $Id$ -Drupal.service = function(endpoint, method, parameters, callback, id) { - var call = {'method': method, 'params': JSON.stringify(parameters)}; - if (id != null) - call['id'] = id; - jQuery.ajax({ - 'url': Drupal.settings.basePath + "?q=" + endpoint, - 'type': "POST", - 'data': call, - 'success': function(data) { - parsed = Drupal.parseJson(data); - callback(parsed['result'], parsed['error'], parsed['id']); - } - }); -} - -//JSON parser and encoder -if(!this.JSON){JSON=function(){function f(n){return n<10?"0"+n:n}Date.prototype.toJSON=function(key){return this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z"};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()};var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapeable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapeable.lastIndex=0;return escapeable.test(string)?'"'+string.replace(escapeable,function(a){var c=meta[a];if(typeof c==="string"){return c}return"\\u"+("0000"+(+(a.charCodeAt(0))).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(typeof value.length==="number"&&!(value.propertyIsEnumerable("length"))){length=value.length;for(i=0;i 'JSON-RPC', 'path' => 'json-rpc', + 'settings' => array( + 'file' => array('inc', 'jsonrpc_server'), + 'form' => '_jsonrpc_server_settings', + 'submit' => '_jsonrpc_server_settings_submit', + ), ); } +/** + * Implements hook_server(). + */ function jsonrpc_server_server() { - require_once('JsonRpcServer.php'); - $GLOBALS['devel_shutdown'] = false; - // TODO: Add settings page with options for disabling features that doesn't - // follow the JSON-RPC spec. One thing that definitely could be disabled is - // the GET request handling, regardless of conformance to spec. + // Remove Devel shutdown. + $GLOBALS['devel_shutdown'] = FALSE; + + // Get the endpoint. + $endpoint_name = services_get_server_info('endpoint'); + $endpoint = services_endpoint_load($endpoint_name); + $settings = $endpoint->server_settings[$endpoint->server]; $method = $_SERVER['REQUEST_METHOD']; - switch ($method) { - case 'POST': - // Omit charset parameter - list($content_type) = explode(';', $_SERVER['CONTENT_TYPE'], 2); - switch($content_type) { - // Handling of a standard JSON-RPC call - case 'application/json': - // We'll use the inputstream module if it's installed because - // otherwise it's only possible to read the input stream once. - // And other parts of services or drupal might want to access it. - if (module_exists('inputstream')) { - $body = file_get_contents('drupal://input'); - } - else { - $body = file_get_contents('php://input'); - } - $in = json_decode($body, TRUE); - break; - // It's not included in the JSON-RPC standard but we support - // sending only the params as JSON. - default: - $in = $_POST; - $in['params'] = json_decode($in['params'], TRUE); - break; + if ($method == 'POST') { + + // Strip out charset if is there. + list($content_type) = explode(';', $_SERVER['CONTENT_TYPE'], 2); + + if (in_array($content_type, array('application/json', 'application/json-rpc', 'application/jsonrequest'))) { + // We'll use the inputstream module if it's installed because + // otherwise it's only possible to read the input stream once. + // And other parts of services or drupal might want to access it. + if (module_exists('inputstream')) { + $body = file_get_contents('drupal://input'); + } + else { + $body = file_get_contents('php://input'); + } + + $input = drupal_json_decode($body); + } + } + + // Allow GET only if was enabled. + elseif ($method == 'GET' && $settings['allow_get_requests']) { + /** + * Collapse multiple parameters with the same name in arrays. + * + * @see http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html#GetProcedureCall + */ + $params = array(); + foreach (explode('&', $_SERVER['QUERY_STRING']) as $pair) { + list($key, $value) = explode('=', $pair, 2); + $key = str_replace('[]', '', $key); + + if (isset($params[$key])) { + if (!is_array($params[$key])) { + $params[$key] = array($params[$key]); + } + $params[$key][] = html_entity_decode($value); + } + else { + $params[$key] = html_entity_decode($value); } - break; - case 'GET': - // This handling of get requests doesn't implement the JSON-RPC spec - // fully. Multiple parameters sharing names are not collapsed into - // arrays. See - // http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html#GetProcedureCall - // for details. - // TODO: Implement custom GET-parameter parsing - $in = array( - 'jsonrpc' => '1.1', - 'method' => basename($_GET['q']), - 'params' => $_GET, - ); - break; + } + + $input = array( + 'version' => '1.1', + 'method' => basename($_GET['q']), + 'params' => $params, + ); } - $in['method'] = trim($in['method'], ' "'); - $server = new JsonRpcServer($in); + // Some cleanup on "method" may be necessary. + $input['method'] = trim($input['method'], ' "'); + + // Start a new server instance. + $server = new JsonRpcServer($input, $method); + try { + // Invoke the server handler. return $server->handle(); - } catch (ServicesException $e) { - return $server->response($e->getData()); } -} - -function jsonrpc_server_add_javascript() { - $path = drupal_get_path("module", "jsonrpc_server"); - drupal_add_js($path ."/jsonrpc_server.js"); + catch (ServicesException $e) { + return $server->buildResponse($e->getData()); + } }