Completed
Push — master ( c3b2cd...52f4ed )
by Marco
04:42
created

JsonProcessor::packJsonSuccess()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 17
ccs 6
cts 6
cp 1
rs 9.4285
cc 2
eloc 8
nc 2
nop 2
crap 2
1
<?php namespace Comodojo\RpcServer\Request;
2
3
use \Comodojo\RpcServer\Request\Parameters;
4
use \Comodojo\RpcServer\Util\DataValidator;
5
use \Comodojo\Exception\RpcException;
6
use \Exception;
7
8
/**
9
 * The JSONRPC processor
10
 *
11
 * @package     Comodojo Spare Parts
12
 * @author      Marco Giovinazzi <[email protected]>
13
 * @license     MIT
14
 *
15
 * LICENSE:
16
 *
17
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
 * THE SOFTWARE.
24
 */
25
26
class JsonProcessor {
27
28
    /**
29
     * A parameters object
30
     *
31
     * @var \Comodojo\RpcServer\Request\Parameters
32
     */
33
    private $parameters = null;
34
35
    /**
36
     * Array of requests
37
     *
38
     * @var array
39
     */
40
    private $requests = array();
41
42
    /**
43
     * Array of results
44
     *
45
     * @var array
46
     */
47
    private $results = array();
48
49
    /**
50
     * Internal flag to identify a batch request
51
     *
52
     * @var bool
53
     */
54
    private $is_batch_request = false;
55
56
    /**
57
     * Current logger
58
     *
59
     * @var \Psr\Log\LoggerInterface
60
     */
61
    private $logger = null;
62
63
    /**
64
     * Class constructor
65
     *
66
     * @param array|object                           $payload
67
     * @param \Comodojo\RpcServer\Request\Parameters $parameters
68
     * @param \Psr\Log\LoggerInterface               $logger
69
     */
70 42
    public function __construct($payload, Parameters $parameters, \Psr\Log\LoggerInterface $logger) {
71
72 42
        $this->logger = $logger;
73
74 42
        $this->logger->notice("Starting JSON processor");
75
76 42
        $this->parameters = $parameters;
77
78 42
        list($this->is_batch_request, $this->requests) = self::preprocessJsonPayload($payload);
79
80 42
    }
81
82
    /**
83
     * Run the processor and exec callback(s)
84
     *
85
     * @return mixed
86
     * @throws Exception
87
     */
88 42
    public function run() {
89
90 42
        foreach ( $this->requests as $request ) {
91
92 42
            if ( isset($request['ERROR_CODE']) && isset($request['ERROR_MESSAGE']) ) {
93
94
                $this->logger->warning("Invalid request ".$request['ID']);
95
96
                $result = self::packJsonError($request['ERROR_CODE'], $request['ERROR_MESSAGE'], $request['ID']);
97
98
                if ( !is_null($result) ) $this->results[] = $result;
99
100
            } else {
101
102
                try {
103
104 42
                    $this->logger->notice("Serving request ".$request['METHOD']."(".$request['ID'].")");
105
106 42
                    $response = $this->runSingleRequest($request['METHOD'], $request['PARAMETERS']);
107
108 33
                    $result = self::packJsonSuccess($response, $request['ID']);
109
110 42
                } catch (RpcException $re) {
111
112 12
                    $this->logger->warning("Error handling request ".$request['ID'].": ".$re->getMessage());
113
114 12
                    $result = self::packJsonError($re->getCode(), $re->getMessage(), $request['ID']);
115
116 12
                } catch (Exception $e) {
117
118
                    $this->logger->error($e->getMessage());
119
120
                    throw $e;
121
122
                }
123
124 42
                if ( !is_null($result) ) $this->results[] = $result;
125
126
            }
127
128
129 42
        }
130
131 42
        if ( empty($this->results) ) {
132
133 3
            return null;
134
135 39
        } else if ( $this->is_batch_request ) {
136
137 9
            return $this->results;
138
139
        } else {
140
141 30
            return $this->results[0];
142
143
        }
144
145
    }
146
147
    /**
148
     * Static constructor - start processor
149
     *
150
     * @param array|object                           $payload
151
     * @param \Comodojo\RpcServer\Request\Parameters $parameters
152
     * @param \Psr\Log\LoggerInterface               $logger
153
     *
154
     * @return mixed
155
     * @throws Exception
156
     */
157 42 View Code Duplication
    public static function process($payload, Parameters $parameters, \Psr\Log\LoggerInterface $logger) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
158
159
        try {
160
161 42
            $processor = new JsonProcessor($payload, $parameters, $logger);
162
163 42
            $return = $processor->run();
164
165 42
        } catch (Exception $e) {
166
167
            throw $e;
168
169
        }
170
171 42
        return $return;
172
173
    }
174
175
    /**
176
     * Preprocess json payload
177
     *
178
     * @param array|object $payload
179
     *
180
     * @return array
181
     */
182 42
    private static function preprocessJsonPayload($payload) {
183
184 42
        $requests = array();
185
186 42
        $is_batch = false;
187
188 42
        if ( is_array($payload) ) {
189
190 9
            $is_batch = true;
191
192 9
            foreach ( $payload as $request ) $requests[] = self::preprocessJsonRequest($request);
193
194 9
        } else {
195
196 33
            $requests[] = self::preprocessJsonRequest($payload);
197
198
        }
199
200 42
        return array($is_batch, $requests);
201
202
    }
203
204
    /**
205
     * Preprocess a single json request
206
     *
207
     * @param array|object $request
208
     *
209
     * @return array
210
     */
211 42
    private static function preprocessJsonRequest($request) {
212
213
        // check for required parameters
214
215
        if (
216 42
            !is_object($request) ||
217 42
            !property_exists($request, 'jsonrpc') ||
218 42
            !property_exists($request, 'method') ||
219 42
            $request->jsonrpc != '2.0' ||
220
            empty($request->method)
221 42
        ) {
222
223
            return array(
224
                'ERROR_CODE' => -32600,
225
                'ERROR_MESSAGE' => 'Invalid Request',
226
                'ID' => !isset($request['id']) ? null : $request['id']
227
            );
228
229
        }
230
231
        // parse request's components
232
233
        return array(
234 42
            'METHOD' => $request->method,
235 42
            'PARAMETERS' => property_exists($request, 'params') ? $request->params : array(),
236 42
            'ID' => property_exists($request, 'id') ? $request->id : null
237 42
        );
238
239
    }
240
241
    /**
242
     * Exec a single request
243
     *
244
     * @param string $request_method
245
     * @param array  $parameters
246
     *
247
     * @return mixed
248
     * @throws RpcException
249
     */
250 42
    private function runSingleRequest($request_method, $parameters) {
251
252
        try {
253
254 42
            $registered_method = $this->checkRequestSustainability($request_method);
255
256 39
            $selected_signature = $this->checkRequestConsistence($registered_method, $parameters);
257
258 39
            if ( is_array($parameters) ) $parameters = self::matchParameters($parameters, $registered_method, $selected_signature);
259
260 39
            $this->parameters->setParameters($parameters);
261
262 39
            $callback = $registered_method->getCallback();
263
264 39
            $method = $registered_method->getMethod();
265
266 42
        } catch (RpcException $re) {
267
268 6
            throw $re;
269
270
        }
271
272 39
        set_error_handler(
273
274 View Code Duplication
            function($severity, $message, $file, $line) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
275
276
                $this->logger->error($message, array(
277
                    "FILE" => $file,
278
                    "LINE" => $line
279
                ));
280
281
                throw new RpcException('Internal error', -32603);
282
283
            }
284
285 39
        );
286
287
        try {
288
289 39
            $return = empty($method) ? call_user_func($callback, $this->parameters) : call_user_func(array($callback, $method), $this->parameters);
290
291 39
        } catch (RpcException $re) {
292
293 6
            restore_error_handler();
294
295 6
            throw $re;
296
297
        } catch (Exception $e) {
298
299
            restore_error_handler();
300
301
            $this->logger->error($e->getMessage(), array(
302
                "FILE" => $e->getFile(),
303
                "LINE" => $e->getLine()
304
            ));
305
306
            throw new RpcException('Internal error', -32603);
307
308
        }
309
310 33
        restore_error_handler();
311
312 33
        return $return;
313
314
    }
315
316
    /**
317
     * Pack a json error response
318
     *
319
     * @param integer $code
320
     * @param string  $message
321
     * @param integer $id
322
     *
323
     * @return array|null
324
     */
325 12
    private static function packJsonError($code, $message, $id) {
326
327 12
        if ( !is_null($id) ) {
328
329
            return array(
330 12
                'jsonrpc' => '2.0',
331
                'error' => array(
332 12
                    'code' => $code,
333
                    'message' => $message
334 12
                ),
335
                'id' => $id
336 12
            );
337
338
        } else {
339
340
            return null;
341
342
        }
343
344
    }
345
346
    /**
347
     * Pack a json success response
348
     *
349
     * @param mixed   $result
350
     * @param integer $id
351
     *
352
     * @return array
353
     */
354 33
    private static function packJsonSuccess($result, $id) {
355
356 33
        if ( !is_null($id) ) {
357
358
            return array(
359 30
                'jsonrpc' => '2.0',
360 30
                'result' => $result,
361
                'id' => $id
362 30
            );
363
364
        } else {
365
366 6
            return null;
367
368
        }
369
370
    }
371
372
    /**
373
     * Create an associative array of $name => $parameter from current signature
374
     *
375
     * @param array                         $provided
376
     * @param \Comodojo\RpcServer\RpcMethod $method
377
     * @param integer                       $selected_signature
378
     *
379
     * @return array
380
     */
381 39 View Code Duplication
    private static function matchParameters($provided, $method, $selected_signature) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
382
383 39
        $parameters = array();
384
385 39
        $requested_parameters = $method->selectSignature($selected_signature)->getParameters();
386
387 39
        $requested_parameters_keys = array_keys($requested_parameters);
388
389 39
        foreach ( $provided as $index => $parameter ) {
390
391 24
            $parameters[$requested_parameters_keys[$index]] = $parameter;
392
393 39
        }
394
395 39
        return $parameters;
396
397
    }
398
399
    /**
400
     * Check if a request is sustainable (i.e. if method is registered)
401
     *
402
     * @param string $request_method
403
     *
404
     * @return \Comodojo\RpcServer\RpcMethod
405
     * @throws \Comodojo\Exception\RpcException
406
     */
407 42 View Code Duplication
    private function checkRequestSustainability($request_method) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
408
409 42
        $method = $this->parameters->methods()->get($request_method);
410
411 42
        if ( is_null($method) ) throw new RpcException("Method not found", -32601);
412
413 39
        return $method;
414
415
    }
416
417
    /**
418
     * Check if a request is consistent (i.e. if it matches one of method's signatures)
419
     *
420
     * @param \Comodojo\RpcServer\RpcMethod $registered_method
421
     * @param array                         $parameters
422
     *
423
     * @return int
424
     * @throws \Comodojo\Exception\RpcException
425
     */
426 39
    private function checkRequestConsistence($registered_method, $parameters) {
427
428 39
        $signatures = $registered_method->getSignatures(false);
429
430 39
        foreach ( $signatures as $num => $signature ) {
431
432 39
            if ( self::checkSignatureMatch($parameters, $signature["PARAMETERS"]) === true ) return $num;
433
434 3
        }
435
436
        throw new RpcException("Invalid params", -32602);
437
438
    }
439
440
    /**
441
     * Check if call match a signature
442
     *
443
     * @param array  $provided
444
     * @param array  $requested
445
     *
446
     * @return bool
447
     */
448 39
    private static function checkSignatureMatch($provided, $requested) {
449
450 39
        if ( is_object($provided) ) {
451
452
            foreach ( $provided as $parameter=>$value ) {
453
454
                if (
455
                    !isset($requested[$parameter]) ||
456
                    !DataValidator::validate($requested[$parameter], $value)
457
                ) return false;
458
459
            }
460
461
        } else {
462
463 39
            $provided_parameters_count = count($provided);
464
465 39
            $requested_parameters_count = count($requested);
466
467 39
            if ( $provided_parameters_count != $requested_parameters_count ) return false;
468
469 39
            $index = 0;
470
471 39
            foreach ($requested as $parameter => $type) {
472
473 24
                if ( !DataValidator::validate($type, $provided[$index]) ) return false;
474
475 24
                $index += 1;
476
477 39
            }
478
479
        }
480
481 39
        return true;
482
483
    }
484
485
}
486