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