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