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