Commit | Line | Data |
---|---|---|
0f643fb2 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
41498ac5 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
0f643fb2 | 5 | | | |
41498ac5 TO |
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 | | |
0f643fb2 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
0f643fb2 TO |
11 | namespace Civi\API; |
12 | ||
132ec342 TO |
13 | use Civi\API\Event\AuthorizeEvent; |
14 | use Civi\API\Event\PrepareEvent; | |
15 | use Civi\API\Event\ExceptionEvent; | |
787604ff | 16 | use Civi\API\Event\ResolveEvent; |
132ec342 TO |
17 | use Civi\API\Event\RespondEvent; |
18 | ||
0f643fb2 | 19 | /** |
0f643fb2 | 20 | * @package Civi |
ca5cec67 | 21 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
0f643fb2 | 22 | */ |
0f643fb2 TO |
23 | class Kernel { |
24 | ||
25 | /** | |
26 | * @var \Symfony\Component\EventDispatcher\EventDispatcher | |
27 | */ | |
28 | protected $dispatcher; | |
29 | ||
30 | /** | |
f33d2b8c | 31 | * @var \Civi\API\Provider\ProviderInterface[] |
0f643fb2 TO |
32 | */ |
33 | protected $apiProviders; | |
34 | ||
6550386a | 35 | /** |
8882ff5c TO |
36 | * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher |
37 | * The event dispatcher which receives kernel events. | |
6550386a | 38 | * @param array $apiProviders |
8882ff5c | 39 | * Array of ProviderInterface. |
6550386a | 40 | */ |
c64f69d9 | 41 | public function __construct($dispatcher, $apiProviders = []) { |
0f643fb2 TO |
42 | $this->apiProviders = $apiProviders; |
43 | $this->dispatcher = $dispatcher; | |
44 | } | |
45 | ||
46 | /** | |
066c4638 | 47 | * @param string $entity |
5a3846f7 | 48 | * Name of entity: e.g. Contact, Activity, Event |
066c4638 | 49 | * @param string $action |
5a3846f7 | 50 | * Name of action: e.g. create, get, delete |
066c4638 TO |
51 | * @param array $params |
52 | * Array to be passed to API function. | |
5a3846f7 | 53 | * |
8bcc0d86 | 54 | * @return array|int |
5a3846f7 | 55 | * @throws \API_Exception |
8bcc0d86 | 56 | * @see runSafe |
5a3846f7 | 57 | * @deprecated |
8bcc0d86 | 58 | */ |
5a3846f7 CW |
59 | public function run($entity, $action, $params) { |
60 | return $this->runSafe($entity, $action, $params); | |
8bcc0d86 CW |
61 | } |
62 | ||
63 | /** | |
64 | * Parse and execute an API request. Any errors will be converted to | |
65 | * normal format. | |
66 | * | |
0f643fb2 | 67 | * @param string $entity |
5a3846f7 | 68 | * Name of entity: e.g. Contact, Activity, Event |
0f643fb2 | 69 | * @param string $action |
5a3846f7 | 70 | * Name of action: e.g. create, get, delete |
0f643fb2 | 71 | * @param array $params |
8882ff5c | 72 | * Array to be passed to API function. |
0f643fb2 TO |
73 | * |
74 | * @return array|int | |
8bcc0d86 | 75 | * @throws \API_Exception |
0f643fb2 | 76 | */ |
5a3846f7 | 77 | public function runSafe($entity, $action, $params) { |
080b7aca | 78 | $apiRequest = []; |
0f643fb2 | 79 | try { |
080b7aca | 80 | $apiRequest = Request::create($entity, $action, $params); |
8bcc0d86 | 81 | $apiResponse = $this->runRequest($apiRequest); |
5fda6437 | 82 | return $this->formatResult($apiRequest, $apiResponse); |
0f643fb2 TO |
83 | } |
84 | catch (\Exception $e) { | |
080b7aca CW |
85 | if ($apiRequest) { |
86 | $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, NULL, $apiRequest, $this)); | |
87 | } | |
91798b7a | 88 | |
91798b7a TO |
89 | if ($e instanceof \PEAR_Exception) { |
90 | $err = $this->formatPearException($e, $apiRequest); | |
8882ff5c TO |
91 | } |
92 | elseif ($e instanceof \API_Exception) { | |
91798b7a | 93 | $err = $this->formatApiException($e, $apiRequest); |
8882ff5c TO |
94 | } |
95 | else { | |
91798b7a | 96 | $err = $this->formatException($e, $apiRequest); |
0f643fb2 | 97 | } |
91798b7a | 98 | |
143ed725 | 99 | return $this->formatResult($apiRequest, $err); |
0f643fb2 | 100 | } |
c65db512 TO |
101 | } |
102 | ||
56154d36 TO |
103 | /** |
104 | * Determine if a hypothetical API call would be authorized. | |
105 | * | |
106 | * @param string $entity | |
8882ff5c | 107 | * Type of entities to deal with. |
56154d36 | 108 | * @param string $action |
8882ff5c | 109 | * Create, get, delete or some special action name. |
56154d36 | 110 | * @param array $params |
8882ff5c | 111 | * Array to be passed to function. |
8bcc0d86 | 112 | * |
8882ff5c TO |
113 | * @return bool |
114 | * TRUE if authorization would succeed. | |
56154d36 TO |
115 | * @throws \Exception |
116 | */ | |
5a3846f7 | 117 | public function runAuthorize($entity, $action, $params) { |
56154d36 | 118 | $apiProvider = NULL; |
5a3846f7 | 119 | $apiRequest = Request::create($entity, $action, $params); |
56154d36 TO |
120 | |
121 | try { | |
6f736521 | 122 | $this->boot($apiRequest); |
56154d36 TO |
123 | list($apiProvider, $apiRequest) = $this->resolve($apiRequest); |
124 | $this->authorize($apiProvider, $apiRequest); | |
8882ff5c | 125 | return TRUE; |
56154d36 TO |
126 | } |
127 | catch (\Civi\API\Exception\UnauthorizedException $e) { | |
8882ff5c | 128 | return FALSE; |
56154d36 TO |
129 | } |
130 | } | |
131 | ||
8bcc0d86 | 132 | /** |
121ec912 | 133 | * Execute an API v3 or v4 request. |
8bcc0d86 CW |
134 | * |
135 | * The request must be in canonical format. Exceptions will be propagated out. | |
136 | * | |
121ec912 CW |
137 | * @param array|\Civi\Api4\Generic\AbstractAction $apiRequest |
138 | * @return array|\Civi\Api4\Generic\Result | |
8bcc0d86 CW |
139 | * @throws \API_Exception |
140 | * @throws \Civi\API\Exception\NotImplementedException | |
141 | * @throws \Civi\API\Exception\UnauthorizedException | |
142 | */ | |
143 | public function runRequest($apiRequest) { | |
7b810209 | 144 | $this->boot($apiRequest); |
8bcc0d86 CW |
145 | $errorScope = \CRM_Core_TemporaryErrorScope::useException(); |
146 | ||
147 | list($apiProvider, $apiRequest) = $this->resolve($apiRequest); | |
148 | $this->authorize($apiProvider, $apiRequest); | |
9abe1c3b | 149 | list ($apiProvider, $apiRequest) = $this->prepare($apiProvider, $apiRequest); |
8bcc0d86 CW |
150 | $result = $apiProvider->invoke($apiRequest); |
151 | ||
7b810209 | 152 | return $this->respond($apiProvider, $apiRequest, $result); |
8bcc0d86 CW |
153 | } |
154 | ||
8882ff5c | 155 | /** |
7b810209 CW |
156 | * Bootstrap - Load basic dependencies and sanity-check inputs. |
157 | * | |
c8ee2f3f | 158 | * @param \Civi\Api4\Generic\AbstractAction|array $apiRequest |
7b810209 | 159 | * @throws \API_Exception |
8882ff5c | 160 | */ |
7b810209 | 161 | public function boot($apiRequest) { |
132ec342 | 162 | require_once 'api/Exception.php'; |
7b810209 | 163 | switch ($apiRequest['version']) { |
7b810209 | 164 | case 3: |
c1a61bef CW |
165 | if (!is_array($apiRequest['params'])) { |
166 | throw new \API_Exception('Input variable `params` is not an array', 2000); | |
167 | } | |
7b810209 CW |
168 | require_once 'api/v3/utils.php'; |
169 | _civicrm_api3_initialize(); | |
170 | break; | |
171 | ||
172 | case 4: | |
173 | // nothing to do | |
174 | break; | |
175 | ||
176 | default: | |
177 | throw new \API_Exception('Unknown api version', 2000); | |
178 | } | |
132ec342 | 179 | } |
91798b7a | 180 | |
8bcc0d86 | 181 | /** |
5ab8eddb | 182 | * @param array $apiRequest |
8bcc0d86 CW |
183 | * @throws \API_Exception |
184 | */ | |
185 | protected function validate($apiRequest) { | |
8bcc0d86 CW |
186 | } |
187 | ||
5fda6437 TO |
188 | /** |
189 | * Determine which, if any, service will execute the API request. | |
190 | * | |
446f0940 | 191 | * @param array $apiRequest |
8882ff5c | 192 | * The full description of the API request. |
446f0940 | 193 | * @throws Exception\NotImplementedException |
5fda6437 | 194 | * @return array |
5ab8eddb TO |
195 | * A tuple with the provider-object and a revised apiRequest. |
196 | * Array(0 => ProviderInterface, 1 => array $apiRequest). | |
5fda6437 TO |
197 | */ |
198 | public function resolve($apiRequest) { | |
34f3bbd9 | 199 | /** @var \Civi\API\Event\ResolveEvent $resolveEvent */ |
c98e9ee3 | 200 | $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest, $this)); |
5fda6437 TO |
201 | $apiRequest = $resolveEvent->getApiRequest(); |
202 | if (!$resolveEvent->getApiProvider()) { | |
fedf821c | 203 | throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)"); |
5fda6437 | 204 | } |
c64f69d9 | 205 | return [$resolveEvent->getApiProvider(), $apiRequest]; |
5fda6437 TO |
206 | } |
207 | ||
208 | /** | |
209 | * Determine if the API request is allowed (under current policy) | |
210 | * | |
34f3bbd9 | 211 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 212 | * The API provider responsible for executing the request. |
5fda6437 | 213 | * @param array $apiRequest |
8882ff5c | 214 | * The full description of the API request. |
446f0940 | 215 | * @throws Exception\UnauthorizedException |
5fda6437 TO |
216 | */ |
217 | public function authorize($apiProvider, $apiRequest) { | |
34f3bbd9 | 218 | /** @var \Civi\API\Event\AuthorizeEvent $event */ |
c98e9ee3 | 219 | $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest, $this)); |
5fda6437 | 220 | if (!$event->isAuthorized()) { |
fedf821c | 221 | throw new \Civi\API\Exception\UnauthorizedException("Authorization failed"); |
5fda6437 TO |
222 | } |
223 | } | |
224 | ||
225 | /** | |
226 | * Allow third-party code to manipulate the API request before execution. | |
227 | * | |
34f3bbd9 | 228 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 229 | * The API provider responsible for executing the request. |
5fda6437 | 230 | * @param array $apiRequest |
8882ff5c | 231 | * The full description of the API request. |
5ab8eddb | 232 | * @return array |
9abe1c3b | 233 | * [0 => ProviderInterface $provider, 1 => array $apiRequest] |
5ab8eddb | 234 | * The revised API request. |
5fda6437 TO |
235 | */ |
236 | public function prepare($apiProvider, $apiRequest) { | |
34f3bbd9 | 237 | /** @var \Civi\API\Event\PrepareEvent $event */ |
c98e9ee3 | 238 | $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest, $this)); |
9abe1c3b | 239 | return [$event->getApiProvider(), $event->getApiRequest()]; |
5fda6437 TO |
240 | } |
241 | ||
242 | /** | |
243 | * Allow third-party code to manipulate the API response after execution. | |
244 | * | |
34f3bbd9 | 245 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 246 | * The API provider responsible for executing the request. |
5fda6437 | 247 | * @param array $apiRequest |
8882ff5c | 248 | * The full description of the API request. |
5fda6437 | 249 | * @param array $result |
8882ff5c | 250 | * The response to return to the client. |
5fda6437 | 251 | * @return mixed |
5ab8eddb | 252 | * The revised $result. |
5fda6437 TO |
253 | */ |
254 | public function respond($apiProvider, $apiRequest, $result) { | |
34f3bbd9 | 255 | /** @var \Civi\API\Event\RespondEvent $event */ |
c98e9ee3 | 256 | $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result, $this)); |
5fda6437 TO |
257 | return $event->getResponse(); |
258 | } | |
259 | ||
82376c19 TO |
260 | /** |
261 | * @param int $version | |
8882ff5c TO |
262 | * API version. |
263 | * @return array | |
264 | * Array<string>. | |
82376c19 TO |
265 | */ |
266 | public function getEntityNames($version) { | |
267 | // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher? | |
c64f69d9 | 268 | $entityNames = []; |
82376c19 | 269 | foreach ($this->getApiProviders() as $provider) { |
34f3bbd9 | 270 | /** @var \Civi\API\Provider\ProviderInterface $provider */ |
82376c19 TO |
271 | $entityNames = array_merge($entityNames, $provider->getEntityNames($version)); |
272 | } | |
273 | $entityNames = array_unique($entityNames); | |
274 | sort($entityNames); | |
275 | return $entityNames; | |
276 | } | |
277 | ||
278 | /** | |
279 | * @param int $version | |
8882ff5c | 280 | * API version. |
82376c19 | 281 | * @param string $entity |
8882ff5c TO |
282 | * API entity. |
283 | * @return array | |
284 | * Array<string> | |
82376c19 TO |
285 | */ |
286 | public function getActionNames($version, $entity) { | |
287 | // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher? | |
c64f69d9 | 288 | $actionNames = []; |
82376c19 | 289 | foreach ($this->getApiProviders() as $provider) { |
34f3bbd9 | 290 | /** @var \Civi\API\Provider\ProviderInterface $provider */ |
82376c19 TO |
291 | $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity)); |
292 | } | |
293 | $actionNames = array_unique($actionNames); | |
294 | sort($actionNames); | |
295 | return $actionNames; | |
296 | } | |
297 | ||
91798b7a TO |
298 | /** |
299 | * @param \Exception $e | |
8882ff5c | 300 | * An unhandled exception. |
91798b7a | 301 | * @param array $apiRequest |
8882ff5c | 302 | * The full description of the API request. |
230a9d58 | 303 | * |
8882ff5c TO |
304 | * @return array |
305 | * API response. | |
230a9d58 | 306 | * @throws \API_Exception |
91798b7a TO |
307 | */ |
308 | public function formatException($e, $apiRequest) { | |
c64f69d9 | 309 | $data = []; |
91798b7a TO |
310 | if (!empty($apiRequest['params']['debug'])) { |
311 | $data['trace'] = $e->getTraceAsString(); | |
312 | } | |
9c465c3b | 313 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
314 | } |
315 | ||
316 | /** | |
317 | * @param \API_Exception $e | |
8882ff5c | 318 | * An unhandled exception. |
91798b7a | 319 | * @param array $apiRequest |
8882ff5c | 320 | * The full description of the API request. |
230a9d58 | 321 | * |
a6c01b45 CW |
322 | * @return array |
323 | * (API response) | |
230a9d58 | 324 | * @throws \API_Exception |
91798b7a TO |
325 | */ |
326 | public function formatApiException($e, $apiRequest) { | |
327 | $data = $e->getExtraParams(); | |
9e10fb6b CW |
328 | $data['entity'] = $apiRequest['entity'] ?? NULL; |
329 | $data['action'] = $apiRequest['action'] ?? NULL; | |
91798b7a TO |
330 | |
331 | if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest)) | |
34f3bbd9 SL |
332 | // prevent recursion |
333 | && empty($data['trace']) | |
91798b7a TO |
334 | ) { |
335 | $data['trace'] = $e->getTraceAsString(); | |
336 | } | |
337 | ||
9c465c3b | 338 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
339 | } |
340 | ||
341 | /** | |
342 | * @param \PEAR_Exception $e | |
8882ff5c | 343 | * An unhandled exception. |
91798b7a | 344 | * @param array $apiRequest |
8882ff5c | 345 | * The full description of the API request. |
230a9d58 | 346 | * |
8882ff5c TO |
347 | * @return array |
348 | * API response. | |
230a9d58 | 349 | * |
350 | * @throws \API_Exception | |
91798b7a TO |
351 | */ |
352 | public function formatPearException($e, $apiRequest) { | |
c64f69d9 | 353 | $data = []; |
91798b7a TO |
354 | $error = $e->getCause(); |
355 | if ($error instanceof \DB_Error) { | |
230a9d58 | 356 | $data['error_code'] = \DB::errorMessage($error->getCode()); |
357 | $data['sql'] = $error->getDebugInfo(); | |
91798b7a TO |
358 | } |
359 | if (!empty($apiRequest['params']['debug'])) { | |
360 | if (method_exists($e, 'getUserInfo')) { | |
361 | $data['debug_info'] = $error->getUserInfo(); | |
362 | } | |
363 | if (method_exists($e, 'getExtraData')) { | |
364 | $data['debug_info'] = $data + $error->getExtraData(); | |
365 | } | |
366 | $data['trace'] = $e->getTraceAsString(); | |
367 | } | |
368 | else { | |
230a9d58 | 369 | $data['tip'] = 'add debug=1 to your API call to have more info about the error'; |
91798b7a TO |
370 | } |
371 | ||
9c465c3b TO |
372 | return $this->createError($e->getMessage(), $data, $apiRequest); |
373 | } | |
374 | ||
375 | /** | |
446f0940 | 376 | * @param string $msg |
8882ff5c | 377 | * Descriptive error message. |
9c465c3b | 378 | * @param array $data |
8882ff5c | 379 | * Error data. |
446f0940 | 380 | * @param array $apiRequest |
8882ff5c TO |
381 | * The full description of the API request. |
382 | * @param mixed $code | |
383 | * Doesn't appear to be used. | |
9c465c3b | 384 | * |
446f0940 | 385 | * @throws \API_Exception |
8882ff5c TO |
386 | * @return array |
387 | * Array<type>. | |
9c465c3b | 388 | */ |
8882ff5c | 389 | public function createError($msg, $data, $apiRequest, $code = NULL) { |
9c465c3b | 390 | // FIXME what to do with $code? |
230a9d58 | 391 | if ($msg === 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') { |
9c465c3b TO |
392 | try { |
393 | $fields = _civicrm_api3_api_getfields($apiRequest); | |
4f94e3fa | 394 | _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields); |
8882ff5c TO |
395 | } |
396 | catch (\Exception $e) { | |
9c465c3b TO |
397 | $msg = $e->getMessage(); |
398 | } | |
399 | } | |
400 | ||
1b136355 | 401 | $data = \civicrm_api3_create_error($msg, $data); |
9c465c3b TO |
402 | |
403 | if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) { | |
404 | $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code']; | |
405 | throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data); | |
406 | } | |
407 | ||
408 | return $data; | |
91798b7a | 409 | } |
143ed725 TO |
410 | |
411 | /** | |
446f0940 | 412 | * @param array $apiRequest |
8882ff5c | 413 | * The full description of the API request. |
446f0940 | 414 | * @param array $result |
8882ff5c | 415 | * The response to return to the client. |
143ed725 TO |
416 | * @return mixed |
417 | */ | |
418 | public function formatResult($apiRequest, $result) { | |
419 | if (isset($apiRequest, $apiRequest['params'])) { | |
420 | if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) { | |
421 | return (empty($result['is_error'])) ? 1 : 0; | |
422 | } | |
423 | ||
424 | if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) { | |
425 | // FIXME dispatch | |
426 | return $result['id']; | |
427 | } | |
428 | } | |
429 | return $result; | |
430 | } | |
82376c19 TO |
431 | |
432 | /** | |
433 | * @return array<ProviderInterface> | |
434 | */ | |
435 | public function getApiProviders() { | |
436 | return $this->apiProviders; | |
437 | } | |
438 | ||
439 | /** | |
440 | * @param array $apiProviders | |
8882ff5c | 441 | * Array<ProviderInterface>. |
70265090 | 442 | * @return Kernel |
82376c19 TO |
443 | */ |
444 | public function setApiProviders($apiProviders) { | |
445 | $this->apiProviders = $apiProviders; | |
70265090 TO |
446 | return $this; |
447 | } | |
448 | ||
449 | /** | |
34f3bbd9 | 450 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 451 | * The API provider responsible for executing the request. |
70265090 TO |
452 | * @return Kernel |
453 | */ | |
454 | public function registerApiProvider($apiProvider) { | |
455 | $this->apiProviders[] = $apiProvider; | |
456 | if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) { | |
457 | $this->getDispatcher()->addSubscriber($apiProvider); | |
458 | } | |
459 | return $this; | |
82376c19 TO |
460 | } |
461 | ||
462 | /** | |
463 | * @return \Symfony\Component\EventDispatcher\EventDispatcher | |
464 | */ | |
465 | public function getDispatcher() { | |
466 | return $this->dispatcher; | |
467 | } | |
468 | ||
469 | /** | |
470 | * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher | |
8882ff5c | 471 | * The event dispatcher which receives kernel events. |
70265090 | 472 | * @return Kernel |
82376c19 TO |
473 | */ |
474 | public function setDispatcher($dispatcher) { | |
475 | $this->dispatcher = $dispatcher; | |
70265090 | 476 | return $this; |
82376c19 | 477 | } |
96025800 | 478 | |
6550386a | 479 | } |