Merge pull request #23710 from eileenmcnaughton/civi_import
[civicrm-core.git] / Civi / Pipe / JsonRpc.php
CommitLineData
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
12namespace Civi\Pipe;
13
14class 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}