Commit | Line | Data |
---|---|---|
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 |
27 | namespace Civi\API; |
28 | ||
132ec342 TO |
29 | use Civi\API\Event\AuthorizeEvent; |
30 | use Civi\API\Event\PrepareEvent; | |
31 | use Civi\API\Event\ExceptionEvent; | |
787604ff | 32 | use Civi\API\Event\ResolveEvent; |
132ec342 TO |
33 | use Civi\API\Event\RespondEvent; |
34 | ||
0f643fb2 | 35 | /** |
0f643fb2 | 36 | * @package Civi |
6b83d5bd | 37 | * @copyright CiviCRM LLC (c) 2004-2019 |
0f643fb2 | 38 | */ |
0f643fb2 TO |
39 | class 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 | } |