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