Merge pull request #16363 from eileenmcnaughton/deextract
[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 /**
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}