This repository has been archived by the owner on Feb 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
JsonRpcServer.php
238 lines (210 loc) · 8.96 KB
/
JsonRpcServer.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
<?php
// $Id$
// References:
// JSON-RPC 1.1 draft: http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html
// JSON-RPC 2.0 proposal: http://groups.google.com/group/json-rpc/web/json-rpc-1-2-proposal
// Error codes follows the json-rpc 2.0 proposal as no codes exists for the 1.1 draft
// -32700 Parse error. Invalid JSON. An error occurred on the server while parsing the JSON text.
// -32600 Invalid Request. The received JSON not a valid JSON-RPC Request.
// -32601 Method not found. The requested remote-procedure does not exist / is not available.
// -32602 Invalid params. Invalid method parameters.
// -32603 Internal error. Internal JSON-RPC error.
// -32099..-32000 Server error. Reserved for implementation-defined server-errors.
define('JSONRPC_ERROR_PARSE', -32700);
define('JSONRPC_ERROR_REQUEST', -32600);
define('JSONRPC_ERROR_PROCEDURE_NOT_FOUND', -32601);
define('JSONRPC_ERROR_PARAMS', -32602);
define('JSONRPC_ERROR_INTERNAL_ERROR', -32603);
class JsonRpcServer{
private $id, $method, $in, $version, $major_version;
private $service_method, $params, $args;
public function __construct($in) {
$this->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'] : NULL;
}
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' => $request)));
}
//If needed, check if parameters can be omitted
$arg_count = count($this->method['args']);
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_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)
// 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)))));
}
}