RpcServer   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 565
Duplicated Lines 0 %

Test Coverage

Coverage 88.89%

Importance

Changes 0
Metric Value
wmc 50
dl 0
loc 565
ccs 120
cts 135
cp 0.8889
rs 8.4
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 33 3
A setErrors() 0 21 2
A setCapabilities() 0 14 2
A setIntrospectionMethods() 0 29 1
A methods() 0 3 1
A errors() 0 3 1
A setPayload() 0 5 1
B can() 0 59 9
A getErrors() 0 3 1
A setProtocol() 0 7 3
A getPayload() 0 3 1
A setEncryption() 0 7 2
A getEncryption() 0 3 1
A setEncoding() 0 5 1
A getCapabilities() 0 3 1
A getMethods() 0 3 1
A getEncoding() 0 3 1
C uncan() 0 55 11
A serve() 0 31 5
A capabilities() 0 3 1
A getProtocol() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like RpcServer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RpcServer, and based on these observations, apply Extract Interface, too.

1
<?php namespace Comodojo\RpcServer;
2
3
use \Comodojo\RpcServer\Component\Capabilities;
4
use \Comodojo\RpcServer\Component\Methods;
5
use \Comodojo\RpcServer\Component\Errors;
6
use \Comodojo\RpcServer\Request\Parameters;
7
use \Comodojo\RpcServer\Request\XmlProcessor;
8
use \Comodojo\RpcServer\Request\JsonProcessor;
9
use \Comodojo\Foundation\Logging\Manager as LogManager;
10
use \Comodojo\Xmlrpc\XmlrpcEncoder;
11
use \Comodojo\Xmlrpc\XmlrpcDecoder;
12
use \phpseclib\Crypt\AES;
13
use \Psr\Log\LoggerInterface;
14
use \Comodojo\Exception\RpcException;
15
use \Comodojo\Exception\XmlrpcException;
16
use \InvalidArgumentException;
17
use \Exception;
18
19
20
/**
21
 * The RpcServer main class.
22
 *
23
 * @package     Comodojo Spare Parts
24
 * @author      Marco Giovinazzi <[email protected]>
25
 * @license     MIT
26
 *
27
 * LICENSE:
28
 *
29
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
 * THE SOFTWARE.
36
 */
37
38
class RpcServer {
39
40
    /**
41
     * Capabilities collector
42
     *
43
     * @const string
44
     */
45
    const XMLRPC = 'xml';
46
47
    /**
48
     * Capabilities collector
49
     *
50
     * @const string
51
     */
52
    const JSONRPC = 'json';
53
54
    /**
55
     * Capabilities collector
56
     *
57
     * @var Capabilities
58
     */
59
    private $capabilities;
60
61
    /**
62
     * RpcMethods collector
63
     *
64
     * @var Methods
65
     */
66
    private $methods;
67
68
    /**
69
     * Standard Rpc Errors collector
70
     *
71
     * @var Errors
72
     */
73
    private $errors;
74
75
    /**
76
     * The request payload, better the RAW export of 'php://input'
77
     *
78
     * @var string
79
     */
80
    private $payload;
81
82
    /**
83
     * Encryption key, in case of encrypted transport
84
     *
85
     * @var string
86
     */
87
    private $encrypt;
88
89
    /**
90
     * Current encoding
91
     *
92
     * @var string
93
     */
94
    private $encoding = 'utf-8';
95
96
    /**
97
     * Current protocol
98
     *
99
     * @var string
100
     */
101
    private $protocol;
102
103
    /**
104
     * Supported RPC protocols
105
     *
106
     * @var array
107
     */
108
    private $supported_protocols = ['xml', 'json'];
109
110
    /**
111
     * Internal marker (encryption)
112
     *
113
     * @var bool
114
     */
115
    private $request_is_encrypted = false;
116
117
    /**
118
     * Current logger
119
     *
120
     * @var LoggerInterface
121
     */
122
    private $logger;
123
124
    /**
125
     * Class constructor
126
     *
127
     * @param string $protocol
128
     *
129
     * @throws Exception
130
     * @throws InvalidArgumentException
131
     */
132 90
    public function __construct($protocol, LoggerInterface $logger = null) {
133
134 90
        $this->logger = is_null($logger) ? LogManager::create('rpcserver', false)->getLogger() : $logger;
135
136
        try {
137
138
            // setup protocol
139
140 90
            $this->setProtocol($protocol);
141
142
            // init components
143
144 87
            $this->capabilities = new Capabilities($this->logger);
145
146 87
            $this->methods = new Methods($this->logger);
147
148 87
            $this->errors = new Errors($this->logger);
149
150
            // populate components
151
152 87
            self::setIntrospectionMethods($this->methods);
153
154 87
            self::setCapabilities($this->capabilities);
155
156 87
            self::setErrors($this->errors);
157
158 3
        } catch (Exception $e) {
159
160 3
            throw $e;
161
162
        }
163
164 87
        $this->logger->debug("RpcServer init complete, protocol $protocol");
165
166 87
    }
167
168
    /**
169
     * Set RPC protocol (json or xml)
170
     *
171
     * @param string $protocol
172
     *
173
     * @return self
174
     * @throws InvalidArgumentException
175
     */
176 90
    public function setProtocol($protocol) {
177
178 90
        if ( empty($protocol) || !in_array($protocol, $this->supported_protocols) ) throw new InvalidArgumentException('Invalid or unsupported RPC protocol');
179
180 87
        $this->protocol = $protocol;
181
182 87
        return $this;
183
184
    }
185
186
    /**
187
     * Get RPC protocol
188
     *
189
     * @return string
190
     */
191 15
    public function getProtocol() {
192
193 15
        return $this->protocol;
194
195
    }
196
197
    /**
198
     * Set request payload, raw format
199
     *
200
     * @return self
201
     */
202 87
    public function setPayload($payload) {
203
204 87
        $this->payload = $payload;
205
206 87
        return $this;
207
208
    }
209
210
    /**
211
     * Get request payload
212
     *
213
     * @return string
214
     */
215 3
    public function getPayload() {
216
217 3
        return $this->payload;
218
219
    }
220
221 3
    public function setEncoding($encoding) {
222
223 3
        $this->encoding = $encoding;
224
225 3
        return $this;
226
227
    }
228
229 3
    public function getEncoding() {
230
231 3
        return $this->encoding;
232
233
    }
234
235
    /**
236
     * Set encryption key; this will enable the NOT-STANDARD payload encryption
237
     *
238
     * @param string $key The encryption key
239
     *
240
     * @return self
241
     * @throws InvalidArgumentException
242
     */
243 6
    public function setEncryption($key) {
244
245 6
        if ( empty($key) ) throw new InvalidArgumentException("Shared key cannot be empty");
246
247 6
        $this->encrypt = $key;
248
249 6
        return $this;
250
251
    }
252
253
    /**
254
     * Get the ecryption key or null if no encryption is selected
255
     *
256
     * @return string
257
     */
258 3
    public function getEncryption() {
259
260 3
        return $this->encrypt;
261
262
    }
263
264
    /**
265
     * Get capabilities object
266
     *
267
     * @deprecated
268
     * @see Parameters::getCapabilities()
269
     * @return Capabilities
270
     */
271 6
    public function capabilities() {
272
273 6
        return $this->getCapabilities();
0 ignored issues
show
Deprecated Code introduced by
The function Comodojo\RpcServer\RpcServer::getCapabilities() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

273
        return /** @scrutinizer ignore-deprecated */ $this->getCapabilities();
Loading history...
274
275
    }
276
277
    /**
278
     * Get capabilities object
279
     *
280
     * @deprecated
281
     * @see Parameters::getCapabilities()
282
     * @return Capabilities
283
     */
284 6
    public function getCapabilities() {
285
286 6
        return $this->capabilities;
287
288
    }
289
290
    /**
291
     * Get methods object
292
     *
293
     * @deprecated
294
     * @see Parameters::getMethods()
295
     * @return Methods
296
     */
297 36
    public function methods() {
298
299 36
        return $this->getMethods();
300
301
    }
302
303
    /**
304
     * Get methods object
305
     *
306
     * @return Methods
307
     */
308 36
    public function getMethods() {
309
310 36
        return $this->methods;
311
312
    }
313
314
    /**
315
     * Get errors object
316
     *
317
     * @deprecated
318
     * @see Parameters::getErrors()
319
     * @return Errors
320
     */
321
    public function errors() {
322
323
        return $this->getErrors();
324
325
    }
326
327
    /**
328
     * Get errors object
329
     *
330
     * @return Errors
331
     */
332
    public function getErrors() {
333
334
        return $this->errors;
335
336
    }
337
338
    /**
339
     * Serve request
340
     *
341
     * @return string
342
     * @throws Exception
343
     */
344 84
    public function serve() {
345
346 84
        $this->logger->notice("Start serving request");
347
348 84
        $parameters_object = new Parameters($this->capabilities, $this->methods, $this->errors, $this->logger, $this->protocol);
349
350
        try {
351
352 84
            $this->logger->debug("Received payload: ".$this->payload);
353
354 84
            $payload = $this->uncan($this->payload);
355
356 84
            $this->logger->debug("Decoded payload", (array) $payload);
357
358 84
            if ( $this->protocol == self::XMLRPC ) $result = XmlProcessor::process($payload, $parameters_object, $this->logger);
359
360 45
            else if ( $this->protocol == self::JSONRPC ) $result = JsonProcessor::process($payload, $parameters_object, $this->logger);
361
362 78
            else throw new Exception('Invalid or unsupported RPC protocol');
363
364 6
        } catch (RpcException $re) {
365
366 6
            return $this->can($re, true);
367
368
        } catch (Exception $e) {
369
370
            throw $e;
371
372
        }
373
374 78
        return $this->can($result, false);
375
376
    }
377
378
    /**
379
     * Uncan the provided payload
380
     *
381
     * @param string $payload
382
     *
383
     * @return mixed
384
     * @throws RpcException
385
     */
386 84
    private function uncan($payload) {
387
388 84
        $decoded = null;
389
390 84
        if ( empty($payload) || !is_string($payload) ) throw new RpcException("Invalid Request", -32600);
391
392 84
        if ( substr($payload, 0, 27) == 'comodojo_encrypted_request-' ) {
393
394 3
            if ( empty($this->encrypt) ) throw new RpcException("Transport error", -32300);
395
396 3
            $this->request_is_encrypted = true;
397
398 3
            $aes = new AES();
399
400 3
            $aes->setKey($this->encrypt);
401
402 3
            $payload = $aes->decrypt(base64_decode(substr($payload, 27)));
403
404 3
            if ( $payload == false ) throw new RpcException("Transport error", -32300);
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $payload of type string to the boolean false. If you are specifically checking for an empty string, consider using the more explicit === '' instead.
Loading history...
405
406
        }
407
408 84
        if ( $this->protocol == 'xml' ) {
409
410 39
            $decoder = new XmlrpcDecoder();
411
412
            try {
413
414 39
                $decoded = $decoder->decodeCall($payload);
415
416
            } catch (XmlrpcException $xe) {
417
418 39
                throw new RpcException("Parse error", -32700);
419
420
            }
421
422 45
        } else if ( $this->protocol == 'json' ) {
423
424 45
            if ( strtolower($this->encoding) != 'utf-8' ) {
425
426
                $payload = mb_convert_encoding($payload, "UTF-8", strtoupper($this->encoding));
427
428
            }
429
430 45
            $decoded = json_decode($payload, false /*DO RAW conversion*/);
431
432 45
            if ( is_null($decoded) ) throw new RpcException("Parse error", -32700);
433
434
        } else {
435
436
            throw new RpcException("Transport error", -32300);
437
438
        }
439
440 84
        return $decoded;
441
442
    }
443
444
    /**
445
     * Can the RPC response
446
     *
447
     * @param mixed   $response
448
     * @param boolean $error
449
     *
450
     * @return string
451
     * @throws RpcException
452
     */
453 84
    private function can($response, $error) {
454
455 84
        $encoded = null;
456
457 84
        if ( $this->protocol == 'xml' ) {
458
459 39
            $encoder = new XmlrpcEncoder();
460
461 39
            $encoder->setEncoding($this->encoding);
462
463
            try {
464
465 39
                $encoded = $error ? $encoder->encodeError($response->getCode(), $response->getMessage()) : $encoder->encodeResponse($response);
466
467
            } catch (XmlrpcException $xe) {
468
469
                $this->logger->error($xe->getMessage());
470
471 39
                $encoded = $encoder->encodeError(-32500, "Application error");
472
473
            }
474
475
        } else {
476
477 45
            if ( strtolower($this->encoding) != 'utf-8' && !is_null($response) ) {
478
479
                array_walk_recursive($response, function(&$entry) {
480
481
                    if ( is_string($entry) ) {
482
483
                        $entry = mb_convert_encoding($entry, strtoupper($this->encoding), "UTF-8");
484
485
                    }
486
487
                });
488
489
            }
490
491
            // json will not return any RpcException; errors (in case) are handled directly by processor
492
493 45
            $encoded = is_null($response) ? null : json_encode($response/*, JSON_NUMERIC_CHECK*/);
494
495
        }
496
497 84
        $this->logger->debug("Plain response: $encoded");
498
499 84
        if ( $this->request_is_encrypted /* && !empty($encoded) */ ) {
500
501 3
            $aes = new AES();
502
503 3
            $aes->setKey($this->encrypt);
504
505 3
            $encoded = 'comodojo_encrypted_response-'.base64_encode($aes->encrypt($encoded));
506
507 3
            $this->logger->debug("Encrypted response: $encoded");
508
509
        }
510
511 84
        return $encoded;
512
513
    }
514
515
    /**
516
     * Inject introspection and reserved RPC methods
517
     *
518
     * @param Methods $methods
519
     */
520 87
    private static function setIntrospectionMethods($methods) {
521
522 87
        $methods->add(RpcMethod::create("system.getCapabilities", '\Comodojo\RpcServer\Reserved\GetCapabilities::execute')
523 87
            ->setDescription("This method lists all the capabilites that the RPC server has: the (more or less standard) extensions to the RPC spec that it adheres to")
524 87
            ->setReturnType('struct')
525
        );
526
527 87
        $methods->add(RpcMethod::create("system.listMethods", '\Comodojo\RpcServer\Introspection\ListMethods::execute')
528 87
            ->setDescription("This method lists all the methods that the RPC server knows how to dispatch")
529 87
            ->setReturnType('array')
530
        );
531
532 87
        $methods->add(RpcMethod::create("system.methodHelp", '\Comodojo\RpcServer\Introspection\MethodHelp::execute')
533 87
            ->setDescription("Returns help text if defined for the method passed, otherwise returns an empty string")
534 87
            ->setReturnType('string')
535 87
            ->addParameter('string', 'method')
536
        );
537
538 87
        $methods->add(RpcMethod::create("system.methodSignature", '\Comodojo\RpcServer\Introspection\MethodSignature::execute')
539 87
            ->setDescription("Returns an array of known signatures (an array of arrays) for the method name passed.".
540 87
                "If no signatures are known, returns a none-array (test for type != array to detect missing signature)")
541 87
            ->setReturnType('array')
542 87
            ->addParameter('string', 'method')
543
        );
544
545 87
        $methods->add(RpcMethod::create("system.multicall", '\Comodojo\RpcServer\Reserved\Multicall::execute')
546 87
            ->setDescription("Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader\$1208 for details")
547 87
            ->setReturnType('array')
548 87
            ->addParameter('array', 'requests')
549
        );
550
551 87
    }
552
553
    /**
554
     * Inject supported capabilities
555
     *
556
     * @param Capabilities $capabilities
557
     */
558 87
    private static function setCapabilities($capabilities) {
559
560
        $supported_capabilities = array(
561 87
            'xmlrpc' => array('http://www.xmlrpc.com/spec', 1),
562
            'system.multicall' => array('http://www.xmlrpc.com/discuss/msgReader$1208', 1),
563
            'introspection' => array('http://phpxmlrpc.sourceforge.net/doc-2/ch10.html', 2),
564
            'nil' => array('http://www.ontosys.com/xml-rpc/extensions.php', 1),
565
            'faults_interop' => array('http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php', 20010516),
566
            'json-rpc' => array('http://www.jsonrpc.org/specification', 2)
567
        );
568
569 87
        foreach ( $supported_capabilities as $capability => $values ) {
570
571 87
            $capabilities->add($capability, $values[0], $values[1]);
572
573
        }
574
575 87
    }
576
577
    /**
578
     * Inject standard and RPC errors
579
     *
580
     * @param Errors $errors
581
     */
582 87
    private static function setErrors($errors) {
583
584
        $std_rpc_errors = array(
585 87
            -32700 => "Parse error",
586
            -32701 => "Parse error - Unsupported encoding",
587
            -32702 => "Parse error - Invalid character for encoding",
588
            -32600 => "Invalid Request",
589
            -32601 => "Method not found",
590
            -32602 => "Invalid params",
591
            -32603 => "Internal error",
592
            -32500 => "Application error",
593
            -32400 => "System error",
594
            -32300 => "Transport error",
595
            // Predefined Comodojo Errors
596
            -31000 => "Multicall is available only in XMLRPC",
597
            -31001 => "Recursive system.multicall forbidden"
598
        );
599
600 87
        foreach ( $std_rpc_errors as $code => $message ) {
601
602 87
            $errors->add($code, $message);
603
604
        }
605
606 87
    }
607
608
 }
609