Commit | Line | Data |
---|---|---|
158d477b TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
4 | | Copyright CiviCRM LLC. All rights reserved. | | |
5 | | | | |
6 | | This work is published under the GNU AGPLv3 license with some | | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
9 | +--------------------------------------------------------------------+ | |
10 | */ | |
11 | ||
12 | namespace Civi\Pipe; | |
13 | ||
14 | class JsonRpc { | |
15 | ||
16 | /** | |
17 | * Execute a JSON-RPC request and return a result. | |
18 | * | |
19 | * This adapter handles decoding, encoding, and conversion of exceptions. | |
20 | * | |
21 | * @code | |
22 | * $input = '{"jsonrpc":"2.0","method":"greet","id":1}'; | |
23 | * $output = JsonRpc::run($input, function(string $method, array $params) { | |
24 | * if ($method === 'greet') return 'hello world'; | |
25 | * else throw new \InvalidArgumentException('Method not found', -32601); | |
26 | * }); | |
27 | * assert $output === '{"jsonrpc":"2.0","result":"hello world","id":1}'; | |
28 | * @endCode | |
29 | * | |
30 | * @param string $requestLine | |
31 | * JSON formatted RPC request | |
32 | * @param callable $dispatcher | |
33 | * Dispatch function - given a parsed/well-formed request, compute the result. | |
34 | * Signature: function(string $method, mixed $params): mixed | |
35 | * @return string | |
36 | * JSON formatted RPC response | |
37 | */ | |
38 | public static function run(string $requestLine, callable $dispatcher): string { | |
39 | $parsed = \json_decode($requestLine, TRUE); | |
40 | ||
41 | if ($parsed === NULL) { | |
42 | throw new \InvalidArgumentException('Parse error', -32700); | |
43 | } | |
44 | ||
45 | if (isset($parsed[0])) { | |
46 | $response = []; | |
47 | foreach ($parsed as $request) { | |
48 | $response[] = static::handleMethodCall($request, $dispatcher); | |
49 | } | |
50 | } | |
51 | elseif (isset($parsed['method'])) { | |
52 | $response = static::handleMethodCall($parsed, $dispatcher); | |
53 | } | |
54 | else { | |
eaa0d7ac | 55 | // [sic] 'Invalid Request' title-case is anomalous but dictated by standard. |
158d477b TO |
56 | throw new \InvalidArgumentException('Invalid Request', -32600); |
57 | } | |
58 | ||
59 | return \json_encode($response); | |
60 | } | |
61 | ||
62 | protected static function handleMethodCall($request, $dispatcher): array { | |
63 | try { | |
64 | if ($request === NULL) { | |
65 | throw new \InvalidArgumentException('Parse error', -32700); | |
66 | } | |
67 | if (($request['jsonrpc'] ?? '') !== '2.0' || !is_string($request['method'])) { | |
eaa0d7ac | 68 | // [sic] 'Invalid Request' title-case is anomalous but dictated by standard. |
158d477b TO |
69 | throw new \InvalidArgumentException('Invalid Request', -32600); |
70 | } | |
6c4b31d4 TO |
71 | if (isset($request['params']) && !is_array($request['params'])) { |
72 | throw new \InvalidArgumentException('Invalid params', -32602); | |
73 | } | |
158d477b TO |
74 | |
75 | $result = $dispatcher($request['method'], $request['params'] ?? []); | |
76 | return static::createResponseSuccess($request, $result); | |
77 | } | |
78 | catch (\Throwable $t) { | |
79 | return static::createResponseError($request, $t); | |
80 | } | |
81 | } | |
82 | ||
83 | /** | |
84 | * Create a response object (successful). | |
85 | * | |
86 | * @link https://www.jsonrpc.org/specification#response_object | |
87 | * @param array{jsonrpc: string, method: string, params: array, id: ?mixed} $request | |
88 | * @param mixed $result | |
89 | * The result-value of the method call. | |
90 | * @return array{jsonrpc: string, result: mixed, id: ?mixed} | |
91 | */ | |
92 | public static function createResponseSuccess(array $request, $result): array { | |
93 | $id = array_key_exists('id', $request) ? ['id' => $request['id']] : []; | |
94 | return [ | |
95 | 'jsonrpc' => '2.0', | |
96 | 'result' => $result, | |
97 | ] + $id; | |
98 | } | |
99 | ||
100 | /** | |
101 | * Create a response object (unsuccessful). | |
102 | * | |
103 | * @link https://www.jsonrpc.org/specification#response_object | |
104 | * @param array{jsonrpc: string, method: string, params: array, id: ?mixed} $request | |
105 | * @param \Throwable $t | |
106 | * The exception which caused the request to fail. | |
107 | * @return array{jsonrpc: string, error: array, id: ?mixed} | |
108 | */ | |
109 | public static function createResponseError(array $request, \Throwable $t): array { | |
110 | $isJsonErrorCode = $t->getCode() >= -32999 && $t->getCode() <= -32000; | |
111 | $errorData = \CRM_Core_Config::singleton()->debug | |
112 | ? ['class' => get_class($t), 'trace' => $t->getTraceAsString()] | |
113 | : NULL; | |
114 | $id = array_key_exists('id', $request) ? ['id' => $request['id']] : []; | |
115 | return [ | |
116 | 'jsonrpc' => $request['jsonrpc'] ?? '2.0', | |
117 | 'error' => [ | |
118 | 'code' => $isJsonErrorCode ? $t->getCode() : -32099, | |
119 | 'message' => $t->getMessage(), | |
120 | 'data' => $errorData, | |
121 | ], | |
122 | ] + $id; | |
123 | } | |
124 | ||
125 | } |