SearchKit - Improve field/operator/value selection UI
[civicrm-core.git] / Civi / API / Kernel.php
CommitLineData
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
11namespace Civi\API;
12
132ec342
TO
13use Civi\API\Event\AuthorizeEvent;
14use Civi\API\Event\PrepareEvent;
15use Civi\API\Event\ExceptionEvent;
787604ff 16use Civi\API\Event\ResolveEvent;
132ec342
TO
17use Civi\API\Event\RespondEvent;
18
0f643fb2 19/**
0f643fb2 20 * @package Civi
ca5cec67 21 * @copyright CiviCRM LLC https://civicrm.org/licensing
0f643fb2 22 */
0f643fb2
TO
23class 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 */
70da3927 221 $event = $this->dispatcher->dispatch('civi.api.authorize', new AuthorizeEvent($apiProvider, $apiRequest, $this, \CRM_Core_Session::getLoggedInContactID() ?: 0));
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}