Commit | Line | Data |
---|---|---|
0f643fb2 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
81621fee | 4 | | CiviCRM version 4.7 | |
0f643fb2 | 5 | +--------------------------------------------------------------------+ |
3b8eef99 | 6 | | Copyright CiviCRM LLC (c) 2004-2017 | |
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 |
3b8eef99 | 38 | * @copyright CiviCRM LLC (c) 2004-2017 |
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 | * | |
156 | * @param $apiRequest | |
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 CW |
202 | /** |
203 | * @param $apiRequest | |
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 |
8882ff5c | 216 | * Array(0 => ProviderInterface, 1 => array). |
5fda6437 TO |
217 | */ |
218 | public function resolve($apiRequest) { | |
446f0940 | 219 | /** @var 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 TO |
224 | } |
225 | return array($resolveEvent->getApiProvider(), $apiRequest); | |
226 | } | |
227 | ||
228 | /** | |
229 | * Determine if the API request is allowed (under current policy) | |
230 | * | |
231 | * @param 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) { | |
446f0940 | 238 | /** @var 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 | * | |
248 | * @param 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. |
5fda6437 TO |
252 | * @return mixed |
253 | */ | |
254 | public function prepare($apiProvider, $apiRequest) { | |
446f0940 | 255 | /** @var PrepareEvent $event */ |
c98e9ee3 | 256 | $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest, $this)); |
5fda6437 TO |
257 | return $event->getApiRequest(); |
258 | } | |
259 | ||
260 | /** | |
261 | * Allow third-party code to manipulate the API response after execution. | |
262 | * | |
263 | * @param ProviderInterface $apiProvider | |
8882ff5c | 264 | * The API provider responsible for executing the request. |
5fda6437 | 265 | * @param array $apiRequest |
8882ff5c | 266 | * The full description of the API request. |
5fda6437 | 267 | * @param array $result |
8882ff5c | 268 | * The response to return to the client. |
5fda6437 TO |
269 | * @return mixed |
270 | */ | |
271 | public function respond($apiProvider, $apiRequest, $result) { | |
446f0940 | 272 | /** @var RespondEvent $event */ |
c98e9ee3 | 273 | $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result, $this)); |
5fda6437 TO |
274 | return $event->getResponse(); |
275 | } | |
276 | ||
82376c19 TO |
277 | /** |
278 | * @param int $version | |
8882ff5c TO |
279 | * API version. |
280 | * @return array | |
281 | * Array<string>. | |
82376c19 TO |
282 | */ |
283 | public function getEntityNames($version) { | |
284 | // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher? | |
285 | $entityNames = array(); | |
286 | foreach ($this->getApiProviders() as $provider) { | |
446f0940 | 287 | /** @var ProviderInterface $provider */ |
82376c19 TO |
288 | $entityNames = array_merge($entityNames, $provider->getEntityNames($version)); |
289 | } | |
290 | $entityNames = array_unique($entityNames); | |
291 | sort($entityNames); | |
292 | return $entityNames; | |
293 | } | |
294 | ||
295 | /** | |
296 | * @param int $version | |
8882ff5c | 297 | * API version. |
82376c19 | 298 | * @param string $entity |
8882ff5c TO |
299 | * API entity. |
300 | * @return array | |
301 | * Array<string> | |
82376c19 TO |
302 | */ |
303 | public function getActionNames($version, $entity) { | |
304 | // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher? | |
305 | $actionNames = array(); | |
306 | foreach ($this->getApiProviders() as $provider) { | |
446f0940 | 307 | /** @var ProviderInterface $provider */ |
82376c19 TO |
308 | $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity)); |
309 | } | |
310 | $actionNames = array_unique($actionNames); | |
311 | sort($actionNames); | |
312 | return $actionNames; | |
313 | } | |
314 | ||
91798b7a TO |
315 | /** |
316 | * @param \Exception $e | |
8882ff5c | 317 | * An unhandled exception. |
91798b7a | 318 | * @param array $apiRequest |
8882ff5c TO |
319 | * The full description of the API request. |
320 | * @return array | |
321 | * API response. | |
91798b7a TO |
322 | */ |
323 | public function formatException($e, $apiRequest) { | |
324 | $data = array(); | |
325 | if (!empty($apiRequest['params']['debug'])) { | |
326 | $data['trace'] = $e->getTraceAsString(); | |
327 | } | |
9c465c3b | 328 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
329 | } |
330 | ||
331 | /** | |
332 | * @param \API_Exception $e | |
8882ff5c | 333 | * An unhandled exception. |
91798b7a | 334 | * @param array $apiRequest |
8882ff5c | 335 | * The full description of the API request. |
a6c01b45 CW |
336 | * @return array |
337 | * (API response) | |
91798b7a TO |
338 | */ |
339 | public function formatApiException($e, $apiRequest) { | |
340 | $data = $e->getExtraParams(); | |
341 | $data['entity'] = \CRM_Utils_Array::value('entity', $apiRequest); | |
342 | $data['action'] = \CRM_Utils_Array::value('action', $apiRequest); | |
343 | ||
344 | if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest)) | |
345 | && empty($data['trace']) // prevent recursion | |
346 | ) { | |
347 | $data['trace'] = $e->getTraceAsString(); | |
348 | } | |
349 | ||
9c465c3b | 350 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
351 | } |
352 | ||
353 | /** | |
354 | * @param \PEAR_Exception $e | |
8882ff5c | 355 | * An unhandled exception. |
91798b7a | 356 | * @param array $apiRequest |
8882ff5c TO |
357 | * The full description of the API request. |
358 | * @return array | |
359 | * API response. | |
91798b7a TO |
360 | */ |
361 | public function formatPearException($e, $apiRequest) { | |
362 | $data = array(); | |
363 | $error = $e->getCause(); | |
364 | if ($error instanceof \DB_Error) { | |
365 | $data["error_code"] = \DB::errorMessage($error->getCode()); | |
366 | $data["sql"] = $error->getDebugInfo(); | |
367 | } | |
368 | if (!empty($apiRequest['params']['debug'])) { | |
369 | if (method_exists($e, 'getUserInfo')) { | |
370 | $data['debug_info'] = $error->getUserInfo(); | |
371 | } | |
372 | if (method_exists($e, 'getExtraData')) { | |
373 | $data['debug_info'] = $data + $error->getExtraData(); | |
374 | } | |
375 | $data['trace'] = $e->getTraceAsString(); | |
376 | } | |
377 | else { | |
378 | $data['tip'] = "add debug=1 to your API call to have more info about the error"; | |
379 | } | |
380 | ||
9c465c3b TO |
381 | return $this->createError($e->getMessage(), $data, $apiRequest); |
382 | } | |
383 | ||
384 | /** | |
446f0940 | 385 | * @param string $msg |
8882ff5c | 386 | * Descriptive error message. |
9c465c3b | 387 | * @param array $data |
8882ff5c | 388 | * Error data. |
446f0940 | 389 | * @param array $apiRequest |
8882ff5c TO |
390 | * The full description of the API request. |
391 | * @param mixed $code | |
392 | * Doesn't appear to be used. | |
9c465c3b | 393 | * |
446f0940 | 394 | * @throws \API_Exception |
8882ff5c TO |
395 | * @return array |
396 | * Array<type>. | |
9c465c3b | 397 | */ |
8882ff5c | 398 | public function createError($msg, $data, $apiRequest, $code = NULL) { |
9c465c3b TO |
399 | // FIXME what to do with $code? |
400 | if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') { | |
401 | try { | |
402 | $fields = _civicrm_api3_api_getfields($apiRequest); | |
4f94e3fa | 403 | _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields); |
8882ff5c TO |
404 | } |
405 | catch (\Exception $e) { | |
9c465c3b TO |
406 | $msg = $e->getMessage(); |
407 | } | |
408 | } | |
409 | ||
410 | $data = civicrm_api3_create_error($msg, $data); | |
411 | ||
412 | if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) { | |
413 | $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code']; | |
414 | throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data); | |
415 | } | |
416 | ||
417 | return $data; | |
91798b7a | 418 | } |
143ed725 TO |
419 | |
420 | /** | |
446f0940 | 421 | * @param array $apiRequest |
8882ff5c | 422 | * The full description of the API request. |
446f0940 | 423 | * @param array $result |
8882ff5c | 424 | * The response to return to the client. |
143ed725 TO |
425 | * @return mixed |
426 | */ | |
427 | public function formatResult($apiRequest, $result) { | |
428 | if (isset($apiRequest, $apiRequest['params'])) { | |
429 | if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) { | |
430 | return (empty($result['is_error'])) ? 1 : 0; | |
431 | } | |
432 | ||
433 | if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) { | |
434 | // FIXME dispatch | |
435 | return $result['id']; | |
436 | } | |
437 | } | |
438 | return $result; | |
439 | } | |
82376c19 TO |
440 | |
441 | /** | |
442 | * @return array<ProviderInterface> | |
443 | */ | |
444 | public function getApiProviders() { | |
445 | return $this->apiProviders; | |
446 | } | |
447 | ||
448 | /** | |
449 | * @param array $apiProviders | |
8882ff5c | 450 | * Array<ProviderInterface>. |
70265090 | 451 | * @return Kernel |
82376c19 TO |
452 | */ |
453 | public function setApiProviders($apiProviders) { | |
454 | $this->apiProviders = $apiProviders; | |
70265090 TO |
455 | return $this; |
456 | } | |
457 | ||
458 | /** | |
459 | * @param ProviderInterface $apiProvider | |
8882ff5c | 460 | * The API provider responsible for executing the request. |
70265090 TO |
461 | * @return Kernel |
462 | */ | |
463 | public function registerApiProvider($apiProvider) { | |
464 | $this->apiProviders[] = $apiProvider; | |
465 | if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) { | |
466 | $this->getDispatcher()->addSubscriber($apiProvider); | |
467 | } | |
468 | return $this; | |
82376c19 TO |
469 | } |
470 | ||
471 | /** | |
472 | * @return \Symfony\Component\EventDispatcher\EventDispatcher | |
473 | */ | |
474 | public function getDispatcher() { | |
475 | return $this->dispatcher; | |
476 | } | |
477 | ||
478 | /** | |
479 | * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher | |
8882ff5c | 480 | * The event dispatcher which receives kernel events. |
70265090 | 481 | * @return Kernel |
82376c19 TO |
482 | */ |
483 | public function setDispatcher($dispatcher) { | |
484 | $this->dispatcher = $dispatcher; | |
70265090 | 485 | return $this; |
82376c19 | 486 | } |
96025800 | 487 | |
6550386a | 488 | } |