Commit | Line | Data |
---|---|---|
0f643fb2 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
fee14197 | 4 | | CiviCRM version 5 | |
0f643fb2 | 5 | +--------------------------------------------------------------------+ |
8c9251b3 | 6 | | Copyright CiviCRM LLC (c) 2004-2018 | |
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 | 33 | use Civi\API\Event\RespondEvent; |
5fda6437 | 34 | use Civi\API\Provider\ProviderInterface; |
132ec342 | 35 | |
0f643fb2 | 36 | /** |
0f643fb2 | 37 | * @package Civi |
8c9251b3 | 38 | * @copyright CiviCRM LLC (c) 2004-2018 |
0f643fb2 | 39 | */ |
0f643fb2 TO |
40 | class Kernel { |
41 | ||
42 | /** | |
43 | * @var \Symfony\Component\EventDispatcher\EventDispatcher | |
44 | */ | |
45 | protected $dispatcher; | |
46 | ||
47 | /** | |
5fda6437 | 48 | * @var array<ProviderInterface> |
0f643fb2 TO |
49 | */ |
50 | protected $apiProviders; | |
51 | ||
6550386a | 52 | /** |
8882ff5c TO |
53 | * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher |
54 | * The event dispatcher which receives kernel events. | |
6550386a | 55 | * @param array $apiProviders |
8882ff5c | 56 | * Array of ProviderInterface. |
6550386a | 57 | */ |
8882ff5c | 58 | public function __construct($dispatcher, $apiProviders = array()) { |
0f643fb2 TO |
59 | $this->apiProviders = $apiProviders; |
60 | $this->dispatcher = $dispatcher; | |
61 | } | |
62 | ||
63 | /** | |
8bcc0d86 | 64 | * @deprecated |
066c4638 TO |
65 | * @param string $entity |
66 | * Type of entities to deal with. | |
67 | * @param string $action | |
68 | * Create, get, delete or some special action name. | |
69 | * @param array $params | |
70 | * Array to be passed to API function. | |
71 | * @param mixed $extra | |
72 | * Unused/deprecated. | |
8bcc0d86 CW |
73 | * @return array|int |
74 | * @see runSafe | |
75 | */ | |
76 | public function run($entity, $action, $params, $extra = NULL) { | |
77 | return $this->runSafe($entity, $action, $params, $extra); | |
78 | } | |
79 | ||
80 | /** | |
81 | * Parse and execute an API request. Any errors will be converted to | |
82 | * normal format. | |
83 | * | |
0f643fb2 | 84 | * @param string $entity |
8882ff5c | 85 | * Type of entities to deal with. |
0f643fb2 | 86 | * @param string $action |
8882ff5c | 87 | * Create, get, delete or some special action name. |
0f643fb2 | 88 | * @param array $params |
8882ff5c TO |
89 | * Array to be passed to API function. |
90 | * @param mixed $extra | |
8bcc0d86 | 91 | * Unused/deprecated. |
0f643fb2 TO |
92 | * |
93 | * @return array|int | |
8bcc0d86 | 94 | * @throws \API_Exception |
0f643fb2 | 95 | */ |
8bcc0d86 | 96 | public function runSafe($entity, $action, $params, $extra = NULL) { |
8bcc0d86 CW |
97 | $apiRequest = Request::create($entity, $action, $params, $extra); |
98 | ||
0f643fb2 | 99 | try { |
8bcc0d86 | 100 | $apiResponse = $this->runRequest($apiRequest); |
5fda6437 | 101 | return $this->formatResult($apiRequest, $apiResponse); |
0f643fb2 TO |
102 | } |
103 | catch (\Exception $e) { | |
8bcc0d86 | 104 | $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, NULL, $apiRequest, $this)); |
91798b7a | 105 | |
91798b7a TO |
106 | if ($e instanceof \PEAR_Exception) { |
107 | $err = $this->formatPearException($e, $apiRequest); | |
8882ff5c TO |
108 | } |
109 | elseif ($e instanceof \API_Exception) { | |
91798b7a | 110 | $err = $this->formatApiException($e, $apiRequest); |
8882ff5c TO |
111 | } |
112 | else { | |
91798b7a | 113 | $err = $this->formatException($e, $apiRequest); |
0f643fb2 | 114 | } |
91798b7a | 115 | |
143ed725 | 116 | return $this->formatResult($apiRequest, $err); |
0f643fb2 | 117 | } |
c65db512 TO |
118 | } |
119 | ||
56154d36 TO |
120 | /** |
121 | * Determine if a hypothetical API call would be authorized. | |
122 | * | |
123 | * @param string $entity | |
8882ff5c | 124 | * Type of entities to deal with. |
56154d36 | 125 | * @param string $action |
8882ff5c | 126 | * Create, get, delete or some special action name. |
56154d36 | 127 | * @param array $params |
8882ff5c TO |
128 | * Array to be passed to function. |
129 | * @param mixed $extra | |
8bcc0d86 CW |
130 | * Unused/deprecated. |
131 | * | |
8882ff5c TO |
132 | * @return bool |
133 | * TRUE if authorization would succeed. | |
56154d36 TO |
134 | * @throws \Exception |
135 | */ | |
136 | public function runAuthorize($entity, $action, $params, $extra = NULL) { | |
137 | $apiProvider = NULL; | |
138 | $apiRequest = Request::create($entity, $action, $params, $extra); | |
139 | ||
140 | try { | |
6f736521 | 141 | $this->boot($apiRequest); |
56154d36 TO |
142 | list($apiProvider, $apiRequest) = $this->resolve($apiRequest); |
143 | $this->authorize($apiProvider, $apiRequest); | |
8882ff5c | 144 | return TRUE; |
56154d36 TO |
145 | } |
146 | catch (\Civi\API\Exception\UnauthorizedException $e) { | |
8882ff5c | 147 | return FALSE; |
56154d36 TO |
148 | } |
149 | } | |
150 | ||
8bcc0d86 CW |
151 | /** |
152 | * Execute an API request. | |
153 | * | |
154 | * The request must be in canonical format. Exceptions will be propagated out. | |
155 | * | |
5ab8eddb | 156 | * @param array $apiRequest |
8bcc0d86 CW |
157 | * @return array |
158 | * @throws \API_Exception | |
159 | * @throws \Civi\API\Exception\NotImplementedException | |
160 | * @throws \Civi\API\Exception\UnauthorizedException | |
161 | */ | |
162 | public function runRequest($apiRequest) { | |
7b810209 | 163 | $this->boot($apiRequest); |
8bcc0d86 CW |
164 | $errorScope = \CRM_Core_TemporaryErrorScope::useException(); |
165 | ||
166 | list($apiProvider, $apiRequest) = $this->resolve($apiRequest); | |
167 | $this->authorize($apiProvider, $apiRequest); | |
168 | $apiRequest = $this->prepare($apiProvider, $apiRequest); | |
169 | $result = $apiProvider->invoke($apiRequest); | |
170 | ||
7b810209 | 171 | return $this->respond($apiProvider, $apiRequest, $result); |
8bcc0d86 CW |
172 | } |
173 | ||
8882ff5c | 174 | /** |
7b810209 CW |
175 | * Bootstrap - Load basic dependencies and sanity-check inputs. |
176 | * | |
177 | * @param \Civi\API\V4\Action|array $apiRequest | |
178 | * @throws \API_Exception | |
8882ff5c | 179 | */ |
7b810209 | 180 | public function boot($apiRequest) { |
132ec342 | 181 | require_once 'api/Exception.php'; |
7b810209 CW |
182 | |
183 | if (!is_array($apiRequest['params'])) { | |
184 | throw new \API_Exception('Input variable `params` is not an array', 2000); | |
185 | } | |
186 | switch ($apiRequest['version']) { | |
187 | case 2: | |
188 | case 3: | |
189 | require_once 'api/v3/utils.php'; | |
190 | _civicrm_api3_initialize(); | |
191 | break; | |
192 | ||
193 | case 4: | |
194 | // nothing to do | |
195 | break; | |
196 | ||
197 | default: | |
198 | throw new \API_Exception('Unknown api version', 2000); | |
199 | } | |
132ec342 | 200 | } |
91798b7a | 201 | |
8bcc0d86 | 202 | /** |
5ab8eddb | 203 | * @param array $apiRequest |
8bcc0d86 CW |
204 | * @throws \API_Exception |
205 | */ | |
206 | protected function validate($apiRequest) { | |
8bcc0d86 CW |
207 | } |
208 | ||
5fda6437 TO |
209 | /** |
210 | * Determine which, if any, service will execute the API request. | |
211 | * | |
446f0940 | 212 | * @param array $apiRequest |
8882ff5c | 213 | * The full description of the API request. |
446f0940 | 214 | * @throws Exception\NotImplementedException |
5fda6437 | 215 | * @return array |
5ab8eddb TO |
216 | * A tuple with the provider-object and a revised apiRequest. |
217 | * Array(0 => ProviderInterface, 1 => array $apiRequest). | |
5fda6437 TO |
218 | */ |
219 | public function resolve($apiRequest) { | |
446f0940 | 220 | /** @var ResolveEvent $resolveEvent */ |
c98e9ee3 | 221 | $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest, $this)); |
5fda6437 TO |
222 | $apiRequest = $resolveEvent->getApiRequest(); |
223 | if (!$resolveEvent->getApiProvider()) { | |
fedf821c | 224 | throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)"); |
5fda6437 TO |
225 | } |
226 | return array($resolveEvent->getApiProvider(), $apiRequest); | |
227 | } | |
228 | ||
229 | /** | |
230 | * Determine if the API request is allowed (under current policy) | |
231 | * | |
232 | * @param ProviderInterface $apiProvider | |
8882ff5c | 233 | * The API provider responsible for executing the request. |
5fda6437 | 234 | * @param array $apiRequest |
8882ff5c | 235 | * The full description of the API request. |
446f0940 | 236 | * @throws Exception\UnauthorizedException |
5fda6437 TO |
237 | */ |
238 | public function authorize($apiProvider, $apiRequest) { | |
446f0940 | 239 | /** @var AuthorizeEvent $event */ |
c98e9ee3 | 240 | $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest, $this)); |
5fda6437 | 241 | if (!$event->isAuthorized()) { |
fedf821c | 242 | throw new \Civi\API\Exception\UnauthorizedException("Authorization failed"); |
5fda6437 TO |
243 | } |
244 | } | |
245 | ||
246 | /** | |
247 | * Allow third-party code to manipulate the API request before execution. | |
248 | * | |
249 | * @param ProviderInterface $apiProvider | |
8882ff5c | 250 | * The API provider responsible for executing the request. |
5fda6437 | 251 | * @param array $apiRequest |
8882ff5c | 252 | * The full description of the API request. |
5ab8eddb TO |
253 | * @return array |
254 | * The revised API request. | |
5fda6437 TO |
255 | */ |
256 | public function prepare($apiProvider, $apiRequest) { | |
446f0940 | 257 | /** @var PrepareEvent $event */ |
c98e9ee3 | 258 | $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest, $this)); |
5fda6437 TO |
259 | return $event->getApiRequest(); |
260 | } | |
261 | ||
262 | /** | |
263 | * Allow third-party code to manipulate the API response after execution. | |
264 | * | |
265 | * @param 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) { | |
446f0940 | 275 | /** @var 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? | |
288 | $entityNames = array(); | |
289 | foreach ($this->getApiProviders() as $provider) { | |
446f0940 | 290 | /** @var 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? | |
308 | $actionNames = array(); | |
309 | foreach ($this->getApiProviders() as $provider) { | |
446f0940 | 310 | /** @var 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) { | |
327 | $data = array(); | |
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)) | |
348 | && empty($data['trace']) // prevent recursion | |
349 | ) { | |
350 | $data['trace'] = $e->getTraceAsString(); | |
351 | } | |
352 | ||
9c465c3b | 353 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
354 | } |
355 | ||
356 | /** | |
357 | * @param \PEAR_Exception $e | |
8882ff5c | 358 | * An unhandled exception. |
91798b7a | 359 | * @param array $apiRequest |
8882ff5c TO |
360 | * The full description of the API request. |
361 | * @return array | |
362 | * API response. | |
91798b7a TO |
363 | */ |
364 | public function formatPearException($e, $apiRequest) { | |
365 | $data = array(); | |
366 | $error = $e->getCause(); | |
367 | if ($error instanceof \DB_Error) { | |
368 | $data["error_code"] = \DB::errorMessage($error->getCode()); | |
369 | $data["sql"] = $error->getDebugInfo(); | |
370 | } | |
371 | if (!empty($apiRequest['params']['debug'])) { | |
372 | if (method_exists($e, 'getUserInfo')) { | |
373 | $data['debug_info'] = $error->getUserInfo(); | |
374 | } | |
375 | if (method_exists($e, 'getExtraData')) { | |
376 | $data['debug_info'] = $data + $error->getExtraData(); | |
377 | } | |
378 | $data['trace'] = $e->getTraceAsString(); | |
379 | } | |
380 | else { | |
381 | $data['tip'] = "add debug=1 to your API call to have more info about the error"; | |
382 | } | |
383 | ||
9c465c3b TO |
384 | return $this->createError($e->getMessage(), $data, $apiRequest); |
385 | } | |
386 | ||
387 | /** | |
446f0940 | 388 | * @param string $msg |
8882ff5c | 389 | * Descriptive error message. |
9c465c3b | 390 | * @param array $data |
8882ff5c | 391 | * Error data. |
446f0940 | 392 | * @param array $apiRequest |
8882ff5c TO |
393 | * The full description of the API request. |
394 | * @param mixed $code | |
395 | * Doesn't appear to be used. | |
9c465c3b | 396 | * |
446f0940 | 397 | * @throws \API_Exception |
8882ff5c TO |
398 | * @return array |
399 | * Array<type>. | |
9c465c3b | 400 | */ |
8882ff5c | 401 | public function createError($msg, $data, $apiRequest, $code = NULL) { |
9c465c3b TO |
402 | // FIXME what to do with $code? |
403 | if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') { | |
404 | try { | |
405 | $fields = _civicrm_api3_api_getfields($apiRequest); | |
4f94e3fa | 406 | _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields); |
8882ff5c TO |
407 | } |
408 | catch (\Exception $e) { | |
9c465c3b TO |
409 | $msg = $e->getMessage(); |
410 | } | |
411 | } | |
412 | ||
1b136355 | 413 | $data = \civicrm_api3_create_error($msg, $data); |
9c465c3b TO |
414 | |
415 | if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) { | |
416 | $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code']; | |
417 | throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data); | |
418 | } | |
419 | ||
420 | return $data; | |
91798b7a | 421 | } |
143ed725 TO |
422 | |
423 | /** | |
446f0940 | 424 | * @param array $apiRequest |
8882ff5c | 425 | * The full description of the API request. |
446f0940 | 426 | * @param array $result |
8882ff5c | 427 | * The response to return to the client. |
143ed725 TO |
428 | * @return mixed |
429 | */ | |
430 | public function formatResult($apiRequest, $result) { | |
431 | if (isset($apiRequest, $apiRequest['params'])) { | |
432 | if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) { | |
433 | return (empty($result['is_error'])) ? 1 : 0; | |
434 | } | |
435 | ||
436 | if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) { | |
437 | // FIXME dispatch | |
438 | return $result['id']; | |
439 | } | |
440 | } | |
441 | return $result; | |
442 | } | |
82376c19 TO |
443 | |
444 | /** | |
445 | * @return array<ProviderInterface> | |
446 | */ | |
447 | public function getApiProviders() { | |
448 | return $this->apiProviders; | |
449 | } | |
450 | ||
451 | /** | |
452 | * @param array $apiProviders | |
8882ff5c | 453 | * Array<ProviderInterface>. |
70265090 | 454 | * @return Kernel |
82376c19 TO |
455 | */ |
456 | public function setApiProviders($apiProviders) { | |
457 | $this->apiProviders = $apiProviders; | |
70265090 TO |
458 | return $this; |
459 | } | |
460 | ||
461 | /** | |
462 | * @param ProviderInterface $apiProvider | |
8882ff5c | 463 | * The API provider responsible for executing the request. |
70265090 TO |
464 | * @return Kernel |
465 | */ | |
466 | public function registerApiProvider($apiProvider) { | |
467 | $this->apiProviders[] = $apiProvider; | |
468 | if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) { | |
469 | $this->getDispatcher()->addSubscriber($apiProvider); | |
470 | } | |
471 | return $this; | |
82376c19 TO |
472 | } |
473 | ||
474 | /** | |
475 | * @return \Symfony\Component\EventDispatcher\EventDispatcher | |
476 | */ | |
477 | public function getDispatcher() { | |
478 | return $this->dispatcher; | |
479 | } | |
480 | ||
481 | /** | |
482 | * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher | |
8882ff5c | 483 | * The event dispatcher which receives kernel events. |
70265090 | 484 | * @return Kernel |
82376c19 TO |
485 | */ |
486 | public function setDispatcher($dispatcher) { | |
487 | $this->dispatcher = $dispatcher; | |
70265090 | 488 | return $this; |
82376c19 | 489 | } |
96025800 | 490 | |
6550386a | 491 | } |