Merge pull request #19248 from eileenmcnaughton/dep_utils
[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 \Civi\API\Provider\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 * @param string $entity
48 * Name of entity: e.g. Contact, Activity, Event
49 * @param string $action
50 * Name of action: e.g. create, get, delete
51 * @param array $params
52 * Array to be passed to API function.
53 *
54 * @return array|int
55 * @throws \API_Exception
56 * @see runSafe
57 * @deprecated
58 */
59 public function run($entity, $action, $params) {
60 return $this->runSafe($entity, $action, $params);
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 * Name of entity: e.g. Contact, Activity, Event
69 * @param string $action
70 * Name of action: e.g. create, get, delete
71 * @param array $params
72 * Array to be passed to API function.
73 *
74 * @return array|int
75 * @throws \API_Exception
76 */
77 public function runSafe($entity, $action, $params) {
78 $apiRequest = [];
79 try {
80 $apiRequest = Request::create($entity, $action, $params);
81 $apiResponse = $this->runRequest($apiRequest);
82 return $this->formatResult($apiRequest, $apiResponse);
83 }
84 catch (\Exception $e) {
85 if ($apiRequest) {
86 $this->dispatcher->dispatch('civi.api.exception', new ExceptionEvent($e, NULL, $apiRequest, $this));
87 }
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 *
113 * @return bool
114 * TRUE if authorization would succeed.
115 * @throws \Exception
116 */
117 public function runAuthorize($entity, $action, $params) {
118 $apiProvider = NULL;
119 $apiRequest = Request::create($entity, $action, $params);
120
121 try {
122 $this->boot($apiRequest);
123 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
124 $this->authorize($apiProvider, $apiRequest);
125 return TRUE;
126 }
127 catch (\Civi\API\Exception\UnauthorizedException $e) {
128 return FALSE;
129 }
130 }
131
132 /**
133 * Execute an API v3 or v4 request.
134 *
135 * The request must be in canonical format. Exceptions will be propagated out.
136 *
137 * @param array|\Civi\Api4\Generic\AbstractAction $apiRequest
138 * @return array|\Civi\Api4\Generic\Result
139 * @throws \API_Exception
140 * @throws \Civi\API\Exception\NotImplementedException
141 * @throws \Civi\API\Exception\UnauthorizedException
142 */
143 public function runRequest($apiRequest) {
144 $this->boot($apiRequest);
145 $errorScope = \CRM_Core_TemporaryErrorScope::useException();
146
147 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
148 $this->authorize($apiProvider, $apiRequest);
149 list ($apiProvider, $apiRequest) = $this->prepare($apiProvider, $apiRequest);
150 $result = $apiProvider->invoke($apiRequest);
151
152 return $this->respond($apiProvider, $apiRequest, $result);
153 }
154
155 /**
156 * Bootstrap - Load basic dependencies and sanity-check inputs.
157 *
158 * @param \Civi\Api4\Generic\AbstractAction|array $apiRequest
159 * @throws \API_Exception
160 */
161 public function boot($apiRequest) {
162 require_once 'api/Exception.php';
163 switch ($apiRequest['version']) {
164 case 3:
165 if (!is_array($apiRequest['params'])) {
166 throw new \API_Exception('Input variable `params` is not an array', 2000);
167 }
168 require_once 'api/v3/utils.php';
169 _civicrm_api3_initialize();
170 break;
171
172 case 4:
173 // nothing to do
174 break;
175
176 default:
177 throw new \API_Exception('Unknown api version', 2000);
178 }
179 }
180
181 /**
182 * @param array $apiRequest
183 * @throws \API_Exception
184 */
185 protected function validate($apiRequest) {
186 }
187
188 /**
189 * Determine which, if any, service will execute the API request.
190 *
191 * @param array $apiRequest
192 * The full description of the API request.
193 * @throws Exception\NotImplementedException
194 * @return array
195 * A tuple with the provider-object and a revised apiRequest.
196 * Array(0 => ProviderInterface, 1 => array $apiRequest).
197 */
198 public function resolve($apiRequest) {
199 /** @var \Civi\API\Event\ResolveEvent $resolveEvent */
200 $resolveEvent = $this->dispatcher->dispatch('civi.api.resolve', new ResolveEvent($apiRequest, $this));
201 $apiRequest = $resolveEvent->getApiRequest();
202 if (!$resolveEvent->getApiProvider()) {
203 throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
204 }
205 return [$resolveEvent->getApiProvider(), $apiRequest];
206 }
207
208 /**
209 * Determine if the API request is allowed (under current policy)
210 *
211 * @param \Civi\API\Provider\ProviderInterface $apiProvider
212 * The API provider responsible for executing the request.
213 * @param array $apiRequest
214 * The full description of the API request.
215 * @throws Exception\UnauthorizedException
216 */
217 public function authorize($apiProvider, $apiRequest) {
218 /** @var \Civi\API\Event\AuthorizeEvent $event */
219 $event = $this->dispatcher->dispatch('civi.api.authorize', new AuthorizeEvent($apiProvider, $apiRequest, $this));
220 if (!$event->isAuthorized()) {
221 throw new \Civi\API\Exception\UnauthorizedException("Authorization failed");
222 }
223 }
224
225 /**
226 * Allow third-party code to manipulate the API request before execution.
227 *
228 * @param \Civi\API\Provider\ProviderInterface $apiProvider
229 * The API provider responsible for executing the request.
230 * @param array $apiRequest
231 * The full description of the API request.
232 * @return array
233 * [0 => ProviderInterface $provider, 1 => array $apiRequest]
234 * The revised API request.
235 */
236 public function prepare($apiProvider, $apiRequest) {
237 /** @var \Civi\API\Event\PrepareEvent $event */
238 $event = $this->dispatcher->dispatch('civi.api.prepare', new PrepareEvent($apiProvider, $apiRequest, $this));
239 return [$event->getApiProvider(), $event->getApiRequest()];
240 }
241
242 /**
243 * Allow third-party code to manipulate the API response after execution.
244 *
245 * @param \Civi\API\Provider\ProviderInterface $apiProvider
246 * The API provider responsible for executing the request.
247 * @param array $apiRequest
248 * The full description of the API request.
249 * @param array $result
250 * The response to return to the client.
251 * @return mixed
252 * The revised $result.
253 */
254 public function respond($apiProvider, $apiRequest, $result) {
255 /** @var \Civi\API\Event\RespondEvent $event */
256 $event = $this->dispatcher->dispatch('civi.api.respond', new RespondEvent($apiProvider, $apiRequest, $result, $this));
257 return $event->getResponse();
258 }
259
260 /**
261 * @param int $version
262 * API version.
263 * @return array
264 * Array<string>.
265 */
266 public function getEntityNames($version) {
267 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
268 $entityNames = [];
269 foreach ($this->getApiProviders() as $provider) {
270 /** @var \Civi\API\Provider\ProviderInterface $provider */
271 $entityNames = array_merge($entityNames, $provider->getEntityNames($version));
272 }
273 $entityNames = array_unique($entityNames);
274 sort($entityNames);
275 return $entityNames;
276 }
277
278 /**
279 * @param int $version
280 * API version.
281 * @param string $entity
282 * API entity.
283 * @return array
284 * Array<string>
285 */
286 public function getActionNames($version, $entity) {
287 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
288 $actionNames = [];
289 foreach ($this->getApiProviders() as $provider) {
290 /** @var \Civi\API\Provider\ProviderInterface $provider */
291 $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity));
292 }
293 $actionNames = array_unique($actionNames);
294 sort($actionNames);
295 return $actionNames;
296 }
297
298 /**
299 * @param \Exception $e
300 * An unhandled exception.
301 * @param array $apiRequest
302 * The full description of the API request.
303 *
304 * @return array
305 * API response.
306 * @throws \API_Exception
307 */
308 public function formatException($e, $apiRequest) {
309 $data = [];
310 if (!empty($apiRequest['params']['debug'])) {
311 $data['trace'] = $e->getTraceAsString();
312 }
313 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
314 }
315
316 /**
317 * @param \API_Exception $e
318 * An unhandled exception.
319 * @param array $apiRequest
320 * The full description of the API request.
321 *
322 * @return array
323 * (API response)
324 * @throws \API_Exception
325 */
326 public function formatApiException($e, $apiRequest) {
327 $data = $e->getExtraParams();
328 $data['entity'] = $apiRequest['entity'] ?? NULL;
329 $data['action'] = $apiRequest['action'] ?? NULL;
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 *
347 * @return array
348 * API response.
349 *
350 * @throws \API_Exception
351 */
352 public function formatPearException($e, $apiRequest) {
353 $data = [];
354 $error = $e->getCause();
355 if ($error instanceof \DB_Error) {
356 $data['error_code'] = \DB::errorMessage($error->getCode());
357 $data['sql'] = $error->getDebugInfo();
358 }
359 if (!empty($apiRequest['params']['debug'])) {
360 if (method_exists($e, 'getUserInfo')) {
361 $data['debug_info'] = $error->getUserInfo();
362 }
363 if (method_exists($e, 'getExtraData')) {
364 $data['debug_info'] = $data + $error->getExtraData();
365 }
366 $data['trace'] = $e->getTraceAsString();
367 }
368 else {
369 $data['tip'] = 'add debug=1 to your API call to have more info about the error';
370 }
371
372 return $this->createError($e->getMessage(), $data, $apiRequest);
373 }
374
375 /**
376 * @param string $msg
377 * Descriptive error message.
378 * @param array $data
379 * Error data.
380 * @param array $apiRequest
381 * The full description of the API request.
382 * @param mixed $code
383 * Doesn't appear to be used.
384 *
385 * @throws \API_Exception
386 * @return array
387 * Array<type>.
388 */
389 public function createError($msg, $data, $apiRequest, $code = NULL) {
390 // FIXME what to do with $code?
391 if ($msg === 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') {
392 try {
393 $fields = _civicrm_api3_api_getfields($apiRequest);
394 _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields);
395 }
396 catch (\Exception $e) {
397 $msg = $e->getMessage();
398 }
399 }
400
401 $data = \civicrm_api3_create_error($msg, $data);
402
403 if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) {
404 $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code'];
405 throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data);
406 }
407
408 return $data;
409 }
410
411 /**
412 * @param array $apiRequest
413 * The full description of the API request.
414 * @param array $result
415 * The response to return to the client.
416 * @return mixed
417 */
418 public function formatResult($apiRequest, $result) {
419 if (isset($apiRequest, $apiRequest['params'])) {
420 if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) {
421 return (empty($result['is_error'])) ? 1 : 0;
422 }
423
424 if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) {
425 // FIXME dispatch
426 return $result['id'];
427 }
428 }
429 return $result;
430 }
431
432 /**
433 * @return array<ProviderInterface>
434 */
435 public function getApiProviders() {
436 return $this->apiProviders;
437 }
438
439 /**
440 * @param array $apiProviders
441 * Array<ProviderInterface>.
442 * @return Kernel
443 */
444 public function setApiProviders($apiProviders) {
445 $this->apiProviders = $apiProviders;
446 return $this;
447 }
448
449 /**
450 * @param \Civi\API\Provider\ProviderInterface $apiProvider
451 * The API provider responsible for executing the request.
452 * @return Kernel
453 */
454 public function registerApiProvider($apiProvider) {
455 $this->apiProviders[] = $apiProvider;
456 if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) {
457 $this->getDispatcher()->addSubscriber($apiProvider);
458 }
459 return $this;
460 }
461
462 /**
463 * @return \Symfony\Component\EventDispatcher\EventDispatcher
464 */
465 public function getDispatcher() {
466 return $this->dispatcher;
467 }
468
469 /**
470 * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
471 * The event dispatcher which receives kernel events.
472 * @return Kernel
473 */
474 public function setDispatcher($dispatcher) {
475 $this->dispatcher = $dispatcher;
476 return $this;
477 }
478
479 }