Commit | Line | Data |
---|---|---|
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 |
11 | namespace Civi\API; |
12 | ||
132ec342 TO |
13 | use Civi\API\Event\AuthorizeEvent; |
14 | use Civi\API\Event\PrepareEvent; | |
15 | use Civi\API\Event\ExceptionEvent; | |
787604ff | 16 | use Civi\API\Event\ResolveEvent; |
132ec342 TO |
17 | use Civi\API\Event\RespondEvent; |
18 | ||
0f643fb2 | 19 | /** |
0f643fb2 | 20 | * @package Civi |
ca5cec67 | 21 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
0f643fb2 | 22 | */ |
0f643fb2 TO |
23 | class 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'; |
7b810209 | 162 | switch ($apiRequest['version']) { |
7b810209 | 163 | case 3: |
c1a61bef CW |
164 | if (!is_array($apiRequest['params'])) { |
165 | throw new \API_Exception('Input variable `params` is not an array', 2000); | |
166 | } | |
7b810209 CW |
167 | require_once 'api/v3/utils.php'; |
168 | _civicrm_api3_initialize(); | |
169 | break; | |
170 | ||
171 | case 4: | |
172 | // nothing to do | |
173 | break; | |
174 | ||
175 | default: | |
176 | throw new \API_Exception('Unknown api version', 2000); | |
177 | } | |
132ec342 | 178 | } |
91798b7a | 179 | |
8bcc0d86 | 180 | /** |
5ab8eddb | 181 | * @param array $apiRequest |
8bcc0d86 CW |
182 | * @throws \API_Exception |
183 | */ | |
184 | protected function validate($apiRequest) { | |
8bcc0d86 CW |
185 | } |
186 | ||
5fda6437 TO |
187 | /** |
188 | * Determine which, if any, service will execute the API request. | |
189 | * | |
446f0940 | 190 | * @param array $apiRequest |
8882ff5c | 191 | * The full description of the API request. |
446f0940 | 192 | * @throws Exception\NotImplementedException |
5fda6437 | 193 | * @return array |
5ab8eddb TO |
194 | * A tuple with the provider-object and a revised apiRequest. |
195 | * Array(0 => ProviderInterface, 1 => array $apiRequest). | |
5fda6437 TO |
196 | */ |
197 | public function resolve($apiRequest) { | |
34f3bbd9 | 198 | /** @var \Civi\API\Event\ResolveEvent $resolveEvent */ |
39b870b8 | 199 | $resolveEvent = $this->dispatcher->dispatch('civi.api.resolve', new ResolveEvent($apiRequest, $this)); |
5fda6437 TO |
200 | $apiRequest = $resolveEvent->getApiRequest(); |
201 | if (!$resolveEvent->getApiProvider()) { | |
fedf821c | 202 | throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)"); |
5fda6437 | 203 | } |
c64f69d9 | 204 | return [$resolveEvent->getApiProvider(), $apiRequest]; |
5fda6437 TO |
205 | } |
206 | ||
207 | /** | |
208 | * Determine if the API request is allowed (under current policy) | |
209 | * | |
34f3bbd9 | 210 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 211 | * The API provider responsible for executing the request. |
5fda6437 | 212 | * @param array $apiRequest |
8882ff5c | 213 | * The full description of the API request. |
446f0940 | 214 | * @throws Exception\UnauthorizedException |
5fda6437 TO |
215 | */ |
216 | public function authorize($apiProvider, $apiRequest) { | |
34f3bbd9 | 217 | /** @var \Civi\API\Event\AuthorizeEvent $event */ |
39b870b8 | 218 | $event = $this->dispatcher->dispatch('civi.api.authorize', new AuthorizeEvent($apiProvider, $apiRequest, $this)); |
5fda6437 | 219 | if (!$event->isAuthorized()) { |
fedf821c | 220 | throw new \Civi\API\Exception\UnauthorizedException("Authorization failed"); |
5fda6437 TO |
221 | } |
222 | } | |
223 | ||
224 | /** | |
225 | * Allow third-party code to manipulate the API request before execution. | |
226 | * | |
34f3bbd9 | 227 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 228 | * The API provider responsible for executing the request. |
5fda6437 | 229 | * @param array $apiRequest |
8882ff5c | 230 | * The full description of the API request. |
5ab8eddb | 231 | * @return array |
9abe1c3b | 232 | * [0 => ProviderInterface $provider, 1 => array $apiRequest] |
5ab8eddb | 233 | * The revised API request. |
5fda6437 TO |
234 | */ |
235 | public function prepare($apiProvider, $apiRequest) { | |
34f3bbd9 | 236 | /** @var \Civi\API\Event\PrepareEvent $event */ |
39b870b8 | 237 | $event = $this->dispatcher->dispatch('civi.api.prepare', new PrepareEvent($apiProvider, $apiRequest, $this)); |
9abe1c3b | 238 | return [$event->getApiProvider(), $event->getApiRequest()]; |
5fda6437 TO |
239 | } |
240 | ||
241 | /** | |
242 | * Allow third-party code to manipulate the API response after execution. | |
243 | * | |
34f3bbd9 | 244 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 245 | * The API provider responsible for executing the request. |
5fda6437 | 246 | * @param array $apiRequest |
8882ff5c | 247 | * The full description of the API request. |
5fda6437 | 248 | * @param array $result |
8882ff5c | 249 | * The response to return to the client. |
5fda6437 | 250 | * @return mixed |
5ab8eddb | 251 | * The revised $result. |
5fda6437 TO |
252 | */ |
253 | public function respond($apiProvider, $apiRequest, $result) { | |
34f3bbd9 | 254 | /** @var \Civi\API\Event\RespondEvent $event */ |
39b870b8 | 255 | $event = $this->dispatcher->dispatch('civi.api.respond', new RespondEvent($apiProvider, $apiRequest, $result, $this)); |
5fda6437 TO |
256 | return $event->getResponse(); |
257 | } | |
258 | ||
82376c19 TO |
259 | /** |
260 | * @param int $version | |
8882ff5c TO |
261 | * API version. |
262 | * @return array | |
263 | * Array<string>. | |
82376c19 TO |
264 | */ |
265 | public function getEntityNames($version) { | |
266 | // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher? | |
c64f69d9 | 267 | $entityNames = []; |
82376c19 | 268 | foreach ($this->getApiProviders() as $provider) { |
34f3bbd9 | 269 | /** @var \Civi\API\Provider\ProviderInterface $provider */ |
82376c19 TO |
270 | $entityNames = array_merge($entityNames, $provider->getEntityNames($version)); |
271 | } | |
272 | $entityNames = array_unique($entityNames); | |
273 | sort($entityNames); | |
274 | return $entityNames; | |
275 | } | |
276 | ||
277 | /** | |
278 | * @param int $version | |
8882ff5c | 279 | * API version. |
82376c19 | 280 | * @param string $entity |
8882ff5c TO |
281 | * API entity. |
282 | * @return array | |
283 | * Array<string> | |
82376c19 TO |
284 | */ |
285 | public function getActionNames($version, $entity) { | |
286 | // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher? | |
c64f69d9 | 287 | $actionNames = []; |
82376c19 | 288 | foreach ($this->getApiProviders() as $provider) { |
34f3bbd9 | 289 | /** @var \Civi\API\Provider\ProviderInterface $provider */ |
82376c19 TO |
290 | $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity)); |
291 | } | |
292 | $actionNames = array_unique($actionNames); | |
293 | sort($actionNames); | |
294 | return $actionNames; | |
295 | } | |
296 | ||
91798b7a TO |
297 | /** |
298 | * @param \Exception $e | |
8882ff5c | 299 | * An unhandled exception. |
91798b7a | 300 | * @param array $apiRequest |
8882ff5c | 301 | * The full description of the API request. |
230a9d58 | 302 | * |
8882ff5c TO |
303 | * @return array |
304 | * API response. | |
230a9d58 | 305 | * @throws \API_Exception |
91798b7a TO |
306 | */ |
307 | public function formatException($e, $apiRequest) { | |
c64f69d9 | 308 | $data = []; |
91798b7a TO |
309 | if (!empty($apiRequest['params']['debug'])) { |
310 | $data['trace'] = $e->getTraceAsString(); | |
311 | } | |
9c465c3b | 312 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
313 | } |
314 | ||
315 | /** | |
316 | * @param \API_Exception $e | |
8882ff5c | 317 | * An unhandled exception. |
91798b7a | 318 | * @param array $apiRequest |
8882ff5c | 319 | * The full description of the API request. |
230a9d58 | 320 | * |
a6c01b45 CW |
321 | * @return array |
322 | * (API response) | |
230a9d58 | 323 | * @throws \API_Exception |
91798b7a TO |
324 | */ |
325 | public function formatApiException($e, $apiRequest) { | |
326 | $data = $e->getExtraParams(); | |
9e10fb6b CW |
327 | $data['entity'] = $apiRequest['entity'] ?? NULL; |
328 | $data['action'] = $apiRequest['action'] ?? NULL; | |
91798b7a TO |
329 | |
330 | if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest)) | |
34f3bbd9 SL |
331 | // prevent recursion |
332 | && empty($data['trace']) | |
91798b7a TO |
333 | ) { |
334 | $data['trace'] = $e->getTraceAsString(); | |
335 | } | |
336 | ||
9c465c3b | 337 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
338 | } |
339 | ||
340 | /** | |
341 | * @param \PEAR_Exception $e | |
8882ff5c | 342 | * An unhandled exception. |
91798b7a | 343 | * @param array $apiRequest |
8882ff5c | 344 | * The full description of the API request. |
230a9d58 | 345 | * |
8882ff5c TO |
346 | * @return array |
347 | * API response. | |
230a9d58 | 348 | * |
349 | * @throws \API_Exception | |
91798b7a TO |
350 | */ |
351 | public function formatPearException($e, $apiRequest) { | |
c64f69d9 | 352 | $data = []; |
91798b7a TO |
353 | $error = $e->getCause(); |
354 | if ($error instanceof \DB_Error) { | |
230a9d58 | 355 | $data['error_code'] = \DB::errorMessage($error->getCode()); |
356 | $data['sql'] = $error->getDebugInfo(); | |
91798b7a TO |
357 | } |
358 | if (!empty($apiRequest['params']['debug'])) { | |
359 | if (method_exists($e, 'getUserInfo')) { | |
360 | $data['debug_info'] = $error->getUserInfo(); | |
361 | } | |
362 | if (method_exists($e, 'getExtraData')) { | |
363 | $data['debug_info'] = $data + $error->getExtraData(); | |
364 | } | |
365 | $data['trace'] = $e->getTraceAsString(); | |
366 | } | |
367 | else { | |
230a9d58 | 368 | $data['tip'] = 'add debug=1 to your API call to have more info about the error'; |
91798b7a TO |
369 | } |
370 | ||
9c465c3b TO |
371 | return $this->createError($e->getMessage(), $data, $apiRequest); |
372 | } | |
373 | ||
374 | /** | |
446f0940 | 375 | * @param string $msg |
8882ff5c | 376 | * Descriptive error message. |
9c465c3b | 377 | * @param array $data |
8882ff5c | 378 | * Error data. |
446f0940 | 379 | * @param array $apiRequest |
8882ff5c TO |
380 | * The full description of the API request. |
381 | * @param mixed $code | |
382 | * Doesn't appear to be used. | |
9c465c3b | 383 | * |
446f0940 | 384 | * @throws \API_Exception |
8882ff5c TO |
385 | * @return array |
386 | * Array<type>. | |
9c465c3b | 387 | */ |
8882ff5c | 388 | public function createError($msg, $data, $apiRequest, $code = NULL) { |
9c465c3b | 389 | // FIXME what to do with $code? |
230a9d58 | 390 | if ($msg === 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') { |
9c465c3b TO |
391 | try { |
392 | $fields = _civicrm_api3_api_getfields($apiRequest); | |
4f94e3fa | 393 | _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields); |
8882ff5c TO |
394 | } |
395 | catch (\Exception $e) { | |
9c465c3b TO |
396 | $msg = $e->getMessage(); |
397 | } | |
398 | } | |
399 | ||
1b136355 | 400 | $data = \civicrm_api3_create_error($msg, $data); |
9c465c3b TO |
401 | |
402 | if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) { | |
403 | $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code']; | |
404 | throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data); | |
405 | } | |
406 | ||
407 | return $data; | |
91798b7a | 408 | } |
143ed725 TO |
409 | |
410 | /** | |
446f0940 | 411 | * @param array $apiRequest |
8882ff5c | 412 | * The full description of the API request. |
446f0940 | 413 | * @param array $result |
8882ff5c | 414 | * The response to return to the client. |
143ed725 TO |
415 | * @return mixed |
416 | */ | |
417 | public function formatResult($apiRequest, $result) { | |
418 | if (isset($apiRequest, $apiRequest['params'])) { | |
419 | if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) { | |
420 | return (empty($result['is_error'])) ? 1 : 0; | |
421 | } | |
422 | ||
423 | if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) { | |
424 | // FIXME dispatch | |
425 | return $result['id']; | |
426 | } | |
427 | } | |
428 | return $result; | |
429 | } | |
82376c19 TO |
430 | |
431 | /** | |
432 | * @return array<ProviderInterface> | |
433 | */ | |
434 | public function getApiProviders() { | |
435 | return $this->apiProviders; | |
436 | } | |
437 | ||
438 | /** | |
439 | * @param array $apiProviders | |
8882ff5c | 440 | * Array<ProviderInterface>. |
70265090 | 441 | * @return Kernel |
82376c19 TO |
442 | */ |
443 | public function setApiProviders($apiProviders) { | |
444 | $this->apiProviders = $apiProviders; | |
70265090 TO |
445 | return $this; |
446 | } | |
447 | ||
448 | /** | |
34f3bbd9 | 449 | * @param \Civi\API\Provider\ProviderInterface $apiProvider |
8882ff5c | 450 | * The API provider responsible for executing the request. |
70265090 TO |
451 | * @return Kernel |
452 | */ | |
453 | public function registerApiProvider($apiProvider) { | |
454 | $this->apiProviders[] = $apiProvider; | |
455 | if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) { | |
456 | $this->getDispatcher()->addSubscriber($apiProvider); | |
457 | } | |
458 | return $this; | |
82376c19 TO |
459 | } |
460 | ||
461 | /** | |
462 | * @return \Symfony\Component\EventDispatcher\EventDispatcher | |
463 | */ | |
464 | public function getDispatcher() { | |
465 | return $this->dispatcher; | |
466 | } | |
467 | ||
468 | /** | |
469 | * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher | |
8882ff5c | 470 | * The event dispatcher which receives kernel events. |
70265090 | 471 | * @return Kernel |
82376c19 TO |
472 | */ |
473 | public function setDispatcher($dispatcher) { | |
474 | $this->dispatcher = $dispatcher; | |
70265090 | 475 | return $this; |
82376c19 | 476 | } |
96025800 | 477 | |
6550386a | 478 | } |