3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
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
;
21 * @copyright CiviCRM LLC https://civicrm.org/licensing
26 * @var \Symfony\Component\EventDispatcher\EventDispatcher
28 protected $dispatcher;
31 * @var \Civi\API\Provider\ProviderInterface[]
33 protected $apiProviders;
36 * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
37 * The event dispatcher which receives kernel events.
38 * @param array $apiProviders
39 * Array of ProviderInterface.
41 public function __construct($dispatcher, $apiProviders = []) {
42 $this->apiProviders
= $apiProviders;
43 $this->dispatcher
= $dispatcher;
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.
55 * @throws \API_Exception
59 public function run($entity, $action, $params) {
60 return $this->runSafe($entity, $action, $params);
64 * Parse and execute an API request. Any errors will be converted to
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.
75 * @throws \API_Exception
77 public function runSafe($entity, $action, $params) {
80 $apiRequest = Request
::create($entity, $action, $params);
81 $apiResponse = $this->runRequest($apiRequest);
82 return $this->formatResult($apiRequest, $apiResponse);
84 catch (\Exception
$e) {
86 $this->dispatcher
->dispatch('civi.api.exception', new ExceptionEvent($e, NULL, $apiRequest, $this));
89 if ($e instanceof \PEAR_Exception
) {
90 $err = $this->formatPearException($e, $apiRequest);
92 elseif ($e instanceof \API_Exception
) {
93 $err = $this->formatApiException($e, $apiRequest);
96 $err = $this->formatException($e, $apiRequest);
99 return $this->formatResult($apiRequest, $err);
104 * Determine if a hypothetical API call would be authorized.
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.
114 * TRUE if authorization would succeed.
117 public function runAuthorize($entity, $action, $params) {
119 $apiRequest = Request
::create($entity, $action, $params);
122 $this->boot($apiRequest);
123 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
124 $this->authorize($apiProvider, $apiRequest);
127 catch (\Civi\API\Exception\UnauthorizedException
$e) {
133 * Execute an API v3 or v4 request.
135 * The request must be in canonical format. Exceptions will be propagated out.
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
143 public function runRequest($apiRequest) {
144 $this->boot($apiRequest);
146 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
147 $this->authorize($apiProvider, $apiRequest);
148 list ($apiProvider, $apiRequest) = $this->prepare($apiProvider, $apiRequest);
149 $result = $apiProvider->invoke($apiRequest);
151 return $this->respond($apiProvider, $apiRequest, $result);
155 * Bootstrap - Load basic dependencies and sanity-check inputs.
157 * @param \Civi\Api4\Generic\AbstractAction|array $apiRequest
158 * @throws \API_Exception
160 public function boot($apiRequest) {
161 require_once 'api/Exception.php';
162 // the create error function loads some functions from utils
163 // so this require is also needed for apiv4 until such time as
164 // we alter create error.
165 require_once 'api/v3/utils.php';
166 switch ($apiRequest['version']) {
168 if (!is_array($apiRequest['params'])) {
169 throw new \
API_Exception('Input variable `params` is not an array', 2000);
171 _civicrm_api3_initialize();
179 throw new \
API_Exception('Unknown api version', 2000);
184 * @param array $apiRequest
185 * @throws \API_Exception
187 protected function validate($apiRequest) {
191 * Determine which, if any, service will execute the API request.
193 * @param array $apiRequest
194 * The full description of the API request.
195 * @throws Exception\NotImplementedException
197 * A tuple with the provider-object and a revised apiRequest.
198 * Array(0 => ProviderInterface, 1 => array $apiRequest).
200 public function resolve($apiRequest) {
201 /** @var \Civi\API\Event\ResolveEvent $resolveEvent */
202 $resolveEvent = $this->dispatcher
->dispatch('civi.api.resolve', new ResolveEvent($apiRequest, $this));
203 $apiRequest = $resolveEvent->getApiRequest();
204 if (!$resolveEvent->getApiProvider()) {
205 throw new \Civi\API\Exception\
NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
207 return [$resolveEvent->getApiProvider(), $apiRequest];
211 * Determine if the API request is allowed (under current policy)
213 * @param \Civi\API\Provider\ProviderInterface $apiProvider
214 * The API provider responsible for executing the request.
215 * @param array $apiRequest
216 * The full description of the API request.
217 * @throws Exception\UnauthorizedException
219 public function authorize($apiProvider, $apiRequest) {
220 /** @var \Civi\API\Event\AuthorizeEvent $event */
221 $event = $this->dispatcher
->dispatch('civi.api.authorize', new AuthorizeEvent($apiProvider, $apiRequest, $this, \CRM_Core_Session
::getLoggedInContactID() ?
: 0));
222 if (!$event->isAuthorized()) {
223 throw new \Civi\API\Exception\
UnauthorizedException("Authorization failed");
228 * Allow third-party code to manipulate the API request before execution.
230 * @param \Civi\API\Provider\ProviderInterface $apiProvider
231 * The API provider responsible for executing the request.
232 * @param array $apiRequest
233 * The full description of the API request.
235 * [0 => ProviderInterface $provider, 1 => array $apiRequest]
236 * The revised API request.
238 public function prepare($apiProvider, $apiRequest) {
239 /** @var \Civi\API\Event\PrepareEvent $event */
240 $event = $this->dispatcher
->dispatch('civi.api.prepare', new PrepareEvent($apiProvider, $apiRequest, $this));
241 return [$event->getApiProvider(), $event->getApiRequest()];
245 * Allow third-party code to manipulate the API response after execution.
247 * @param \Civi\API\Provider\ProviderInterface $apiProvider
248 * The API provider responsible for executing the request.
249 * @param array $apiRequest
250 * The full description of the API request.
251 * @param array $result
252 * The response to return to the client.
254 * The revised $result.
256 public function respond($apiProvider, $apiRequest, $result) {
257 /** @var \Civi\API\Event\RespondEvent $event */
258 $event = $this->dispatcher
->dispatch('civi.api.respond', new RespondEvent($apiProvider, $apiRequest, $result, $this));
259 return $event->getResponse();
263 * @param int $version
268 public function getEntityNames($version) {
269 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
271 foreach ($this->getApiProviders() as $provider) {
272 /** @var \Civi\API\Provider\ProviderInterface $provider */
273 $entityNames = array_merge($entityNames, $provider->getEntityNames($version));
275 $entityNames = array_unique($entityNames);
281 * @param int $version
283 * @param string $entity
288 public function getActionNames($version, $entity) {
289 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
291 foreach ($this->getApiProviders() as $provider) {
292 /** @var \Civi\API\Provider\ProviderInterface $provider */
293 $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity));
295 $actionNames = array_unique($actionNames);
301 * @param \Exception $e
302 * An unhandled exception.
303 * @param array $apiRequest
304 * The full description of the API request.
308 * @throws \API_Exception
310 public function formatException($e, $apiRequest) {
312 if (!empty($apiRequest['params']['debug'])) {
313 $data['trace'] = $e->getTraceAsString();
315 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
319 * @param \API_Exception $e
320 * An unhandled exception.
321 * @param array $apiRequest
322 * The full description of the API request.
326 * @throws \API_Exception
328 public function formatApiException($e, $apiRequest) {
329 $data = $e->getExtraParams();
330 $data['entity'] = $apiRequest['entity'] ??
NULL;
331 $data['action'] = $apiRequest['action'] ??
NULL;
333 if (\CRM_Utils_Array
::value('debug', \CRM_Utils_Array
::value('params', $apiRequest))
335 && empty($data['trace'])
337 $data['trace'] = $e->getTraceAsString();
340 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
344 * @param \PEAR_Exception $e
345 * An unhandled exception.
346 * @param array $apiRequest
347 * The full description of the API request.
352 * @throws \API_Exception
354 public function formatPearException($e, $apiRequest) {
356 $error = $e->getCause();
357 if ($error instanceof \DB_Error
) {
358 $data['error_code'] = \DB
::errorMessage($error->getCode());
359 $data['sql'] = $error->getDebugInfo();
361 if (!empty($apiRequest['params']['debug'])) {
362 if (method_exists($e, 'getUserInfo')) {
363 $data['debug_info'] = $error->getUserInfo();
365 if (method_exists($e, 'getExtraData')) {
366 $data['debug_info'] = $data +
$error->getExtraData();
368 $data['trace'] = $e->getTraceAsString();
371 $data['tip'] = 'add debug=1 to your API call to have more info about the error';
374 return $this->createError($e->getMessage(), $data, $apiRequest);
379 * Descriptive error message.
382 * @param array $apiRequest
383 * The full description of the API request.
385 * Doesn't appear to be used.
387 * @throws \API_Exception
391 public function createError($msg, $data, $apiRequest, $code = NULL) {
392 // FIXME what to do with $code?
393 if ($msg === 'DB Error: constraint violation' ||
substr($msg, 0, 9) == 'DB Error:' ||
$msg == 'DB Error: already exists') {
395 $fields = _civicrm_api3_api_getfields($apiRequest);
396 _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields);
398 catch (\Exception
$e) {
399 $msg = $e->getMessage();
403 $data = \
civicrm_api3_create_error($msg, $data);
405 if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) {
406 $errorCode = empty($data['error_code']) ?
'chained_api_failed' : $data['error_code'];
407 throw new \
API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data);
414 * @param array $apiRequest
415 * The full description of the API request.
416 * @param array $result
417 * The response to return to the client.
420 public function formatResult($apiRequest, $result) {
421 if (isset($apiRequest, $apiRequest['params'])) {
422 if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) {
423 return (empty($result['is_error'])) ?
1 : 0;
426 if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) {
428 return $result['id'];
435 * @return array<ProviderInterface>
437 public function getApiProviders() {
438 return $this->apiProviders
;
442 * @param array $apiProviders
443 * Array<ProviderInterface>.
446 public function setApiProviders($apiProviders) {
447 $this->apiProviders
= $apiProviders;
452 * @param \Civi\API\Provider\ProviderInterface $apiProvider
453 * The API provider responsible for executing the request.
456 public function registerApiProvider($apiProvider) {
457 $this->apiProviders
[] = $apiProvider;
458 if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface
) {
459 $this->getDispatcher()->addSubscriber($apiProvider);
465 * @return \Symfony\Component\EventDispatcher\EventDispatcher
467 public function getDispatcher() {
468 return $this->dispatcher
;
472 * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
473 * The event dispatcher which receives kernel events.
476 public function setDispatcher($dispatcher) {
477 $this->dispatcher
= $dispatcher;