Merge pull request #9198 from colemanw/CRM-19448-fix
[civicrm-core.git] / Civi / API / Kernel.php
CommitLineData
0f643fb2
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
81621fee 4 | CiviCRM version 4.7 |
0f643fb2 5 +--------------------------------------------------------------------+
fa938177 6 | Copyright CiviCRM LLC (c) 2004-2016 |
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
27namespace Civi\API;
28
132ec342
TO
29use Civi\API\Event\AuthorizeEvent;
30use Civi\API\Event\PrepareEvent;
31use Civi\API\Event\ExceptionEvent;
787604ff 32use Civi\API\Event\ResolveEvent;
132ec342 33use Civi\API\Event\RespondEvent;
5fda6437 34use Civi\API\Provider\ProviderInterface;
132ec342 35
0f643fb2 36/**
0f643fb2 37 * @package Civi
fa938177 38 * @copyright CiviCRM LLC (c) 2004-2016
0f643fb2 39 */
0f643fb2
TO
40class 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 /**
64 * @param string $entity
8882ff5c 65 * Type of entities to deal with.
0f643fb2 66 * @param string $action
8882ff5c 67 * Create, get, delete or some special action name.
0f643fb2 68 * @param array $params
8882ff5c
TO
69 * Array to be passed to API function.
70 * @param mixed $extra
71 * Who knows.
0f643fb2
TO
72 *
73 * @return array|int
74 */
70265090 75 public function run($entity, $action, $params, $extra = NULL) {
787604ff
TO
76 /**
77 * @var $apiProvider \Civi\API\Provider\ProviderInterface|NULL
78 */
79 $apiProvider = NULL;
80
70be69e2
TO
81 // TODO Define alternative calling convention makes it easier to construct $apiRequest
82 // without the ambiguity of "data" vs "options"
d3159a21 83 $apiRequest = Request::create($entity, $action, $params, $extra);
0f643fb2 84
0f643fb2 85 try {
0f643fb2
TO
86 if (!is_array($params)) {
87 throw new \API_Exception('Input variable `params` is not an array', 2000);
88 }
132ec342
TO
89
90 $this->boot();
0f643fb2 91 $errorScope = \CRM_Core_TemporaryErrorScope::useException();
132ec342 92
5fda6437
TO
93 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
94 $this->authorize($apiProvider, $apiRequest);
95 $apiRequest = $this->prepare($apiProvider, $apiRequest);
787604ff
TO
96 $result = $apiProvider->invoke($apiRequest);
97
5fda6437
TO
98 $apiResponse = $this->respond($apiProvider, $apiRequest, $result);
99 return $this->formatResult($apiRequest, $apiResponse);
0f643fb2
TO
100 }
101 catch (\Exception $e) {
c98e9ee3 102 $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, $apiProvider, $apiRequest, $this));
91798b7a 103
91798b7a
TO
104 if ($e instanceof \PEAR_Exception) {
105 $err = $this->formatPearException($e, $apiRequest);
8882ff5c
TO
106 }
107 elseif ($e instanceof \API_Exception) {
91798b7a 108 $err = $this->formatApiException($e, $apiRequest);
8882ff5c
TO
109 }
110 else {
91798b7a 111 $err = $this->formatException($e, $apiRequest);
0f643fb2 112 }
91798b7a 113
143ed725 114 return $this->formatResult($apiRequest, $err);
0f643fb2 115 }
c65db512
TO
116 }
117
56154d36
TO
118 /**
119 * Determine if a hypothetical API call would be authorized.
120 *
121 * @param string $entity
8882ff5c 122 * Type of entities to deal with.
56154d36 123 * @param string $action
8882ff5c 124 * Create, get, delete or some special action name.
56154d36 125 * @param array $params
8882ff5c
TO
126 * Array to be passed to function.
127 * @param mixed $extra
128 * Who knows.
129 * @return bool
130 * TRUE if authorization would succeed.
56154d36
TO
131 * @throws \Exception
132 */
133 public function runAuthorize($entity, $action, $params, $extra = NULL) {
134 $apiProvider = NULL;
135 $apiRequest = Request::create($entity, $action, $params, $extra);
136
137 try {
138 $this->boot();
139 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
140 $this->authorize($apiProvider, $apiRequest);
8882ff5c 141 return TRUE;
56154d36
TO
142 }
143 catch (\Civi\API\Exception\UnauthorizedException $e) {
8882ff5c 144 return FALSE;
56154d36
TO
145 }
146 }
147
8882ff5c
TO
148 /**
149 * Bootstrap - Load basic dependencies.
150 */
132ec342 151 public function boot() {
8882ff5c 152 require_once 'api/v3/utils.php';
132ec342
TO
153 require_once 'api/Exception.php';
154 _civicrm_api3_initialize();
155 }
91798b7a 156
5fda6437
TO
157 /**
158 * Determine which, if any, service will execute the API request.
159 *
446f0940 160 * @param array $apiRequest
8882ff5c 161 * The full description of the API request.
446f0940 162 * @throws Exception\NotImplementedException
5fda6437 163 * @return array
8882ff5c 164 * Array(0 => ProviderInterface, 1 => array).
5fda6437
TO
165 */
166 public function resolve($apiRequest) {
446f0940 167 /** @var ResolveEvent $resolveEvent */
c98e9ee3 168 $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest, $this));
5fda6437
TO
169 $apiRequest = $resolveEvent->getApiRequest();
170 if (!$resolveEvent->getApiProvider()) {
fedf821c 171 throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
5fda6437
TO
172 }
173 return array($resolveEvent->getApiProvider(), $apiRequest);
174 }
175
176 /**
177 * Determine if the API request is allowed (under current policy)
178 *
179 * @param ProviderInterface $apiProvider
8882ff5c 180 * The API provider responsible for executing the request.
5fda6437 181 * @param array $apiRequest
8882ff5c 182 * The full description of the API request.
446f0940 183 * @throws Exception\UnauthorizedException
5fda6437
TO
184 */
185 public function authorize($apiProvider, $apiRequest) {
446f0940 186 /** @var AuthorizeEvent $event */
c98e9ee3 187 $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest, $this));
5fda6437 188 if (!$event->isAuthorized()) {
fedf821c 189 throw new \Civi\API\Exception\UnauthorizedException("Authorization failed");
5fda6437
TO
190 }
191 }
192
193 /**
194 * Allow third-party code to manipulate the API request before execution.
195 *
196 * @param ProviderInterface $apiProvider
8882ff5c 197 * The API provider responsible for executing the request.
5fda6437 198 * @param array $apiRequest
8882ff5c 199 * The full description of the API request.
5fda6437
TO
200 * @return mixed
201 */
202 public function prepare($apiProvider, $apiRequest) {
446f0940 203 /** @var PrepareEvent $event */
c98e9ee3 204 $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest, $this));
5fda6437
TO
205 return $event->getApiRequest();
206 }
207
208 /**
209 * Allow third-party code to manipulate the API response after execution.
210 *
211 * @param ProviderInterface $apiProvider
8882ff5c 212 * The API provider responsible for executing the request.
5fda6437 213 * @param array $apiRequest
8882ff5c 214 * The full description of the API request.
5fda6437 215 * @param array $result
8882ff5c 216 * The response to return to the client.
5fda6437
TO
217 * @return mixed
218 */
219 public function respond($apiProvider, $apiRequest, $result) {
446f0940 220 /** @var RespondEvent $event */
c98e9ee3 221 $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result, $this));
5fda6437
TO
222 return $event->getResponse();
223 }
224
82376c19
TO
225 /**
226 * @param int $version
8882ff5c
TO
227 * API version.
228 * @return array
229 * Array<string>.
82376c19
TO
230 */
231 public function getEntityNames($version) {
232 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
233 $entityNames = array();
234 foreach ($this->getApiProviders() as $provider) {
446f0940 235 /** @var ProviderInterface $provider */
82376c19
TO
236 $entityNames = array_merge($entityNames, $provider->getEntityNames($version));
237 }
238 $entityNames = array_unique($entityNames);
239 sort($entityNames);
240 return $entityNames;
241 }
242
243 /**
244 * @param int $version
8882ff5c 245 * API version.
82376c19 246 * @param string $entity
8882ff5c
TO
247 * API entity.
248 * @return array
249 * Array<string>
82376c19
TO
250 */
251 public function getActionNames($version, $entity) {
252 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
253 $actionNames = array();
254 foreach ($this->getApiProviders() as $provider) {
446f0940 255 /** @var ProviderInterface $provider */
82376c19
TO
256 $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity));
257 }
258 $actionNames = array_unique($actionNames);
259 sort($actionNames);
260 return $actionNames;
261 }
262
91798b7a
TO
263 /**
264 * @param \Exception $e
8882ff5c 265 * An unhandled exception.
91798b7a 266 * @param array $apiRequest
8882ff5c
TO
267 * The full description of the API request.
268 * @return array
269 * API response.
91798b7a
TO
270 */
271 public function formatException($e, $apiRequest) {
272 $data = array();
273 if (!empty($apiRequest['params']['debug'])) {
274 $data['trace'] = $e->getTraceAsString();
275 }
9c465c3b 276 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
91798b7a
TO
277 }
278
279 /**
280 * @param \API_Exception $e
8882ff5c 281 * An unhandled exception.
91798b7a 282 * @param array $apiRequest
8882ff5c 283 * The full description of the API request.
a6c01b45
CW
284 * @return array
285 * (API response)
91798b7a
TO
286 */
287 public function formatApiException($e, $apiRequest) {
288 $data = $e->getExtraParams();
289 $data['entity'] = \CRM_Utils_Array::value('entity', $apiRequest);
290 $data['action'] = \CRM_Utils_Array::value('action', $apiRequest);
291
292 if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest))
293 && empty($data['trace']) // prevent recursion
294 ) {
295 $data['trace'] = $e->getTraceAsString();
296 }
297
9c465c3b 298 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
91798b7a
TO
299 }
300
301 /**
302 * @param \PEAR_Exception $e
8882ff5c 303 * An unhandled exception.
91798b7a 304 * @param array $apiRequest
8882ff5c
TO
305 * The full description of the API request.
306 * @return array
307 * API response.
91798b7a
TO
308 */
309 public function formatPearException($e, $apiRequest) {
310 $data = array();
311 $error = $e->getCause();
312 if ($error instanceof \DB_Error) {
313 $data["error_code"] = \DB::errorMessage($error->getCode());
314 $data["sql"] = $error->getDebugInfo();
315 }
316 if (!empty($apiRequest['params']['debug'])) {
317 if (method_exists($e, 'getUserInfo')) {
318 $data['debug_info'] = $error->getUserInfo();
319 }
320 if (method_exists($e, 'getExtraData')) {
321 $data['debug_info'] = $data + $error->getExtraData();
322 }
323 $data['trace'] = $e->getTraceAsString();
324 }
325 else {
326 $data['tip'] = "add debug=1 to your API call to have more info about the error";
327 }
328
9c465c3b
TO
329 return $this->createError($e->getMessage(), $data, $apiRequest);
330 }
331
332 /**
446f0940 333 * @param string $msg
8882ff5c 334 * Descriptive error message.
9c465c3b 335 * @param array $data
8882ff5c 336 * Error data.
446f0940 337 * @param array $apiRequest
8882ff5c
TO
338 * The full description of the API request.
339 * @param mixed $code
340 * Doesn't appear to be used.
9c465c3b 341 *
446f0940 342 * @throws \API_Exception
8882ff5c
TO
343 * @return array
344 * Array<type>.
9c465c3b 345 */
8882ff5c 346 public function createError($msg, $data, $apiRequest, $code = NULL) {
9c465c3b
TO
347 // FIXME what to do with $code?
348 if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') {
349 try {
350 $fields = _civicrm_api3_api_getfields($apiRequest);
4f94e3fa 351 _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields);
8882ff5c
TO
352 }
353 catch (\Exception $e) {
9c465c3b
TO
354 $msg = $e->getMessage();
355 }
356 }
357
358 $data = civicrm_api3_create_error($msg, $data);
359
360 if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) {
361 $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code'];
362 throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data);
363 }
364
365 return $data;
91798b7a 366 }
143ed725
TO
367
368 /**
446f0940 369 * @param array $apiRequest
8882ff5c 370 * The full description of the API request.
446f0940 371 * @param array $result
8882ff5c 372 * The response to return to the client.
143ed725
TO
373 * @return mixed
374 */
375 public function formatResult($apiRequest, $result) {
376 if (isset($apiRequest, $apiRequest['params'])) {
377 if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) {
378 return (empty($result['is_error'])) ? 1 : 0;
379 }
380
381 if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) {
382 // FIXME dispatch
383 return $result['id'];
384 }
385 }
386 return $result;
387 }
82376c19
TO
388
389 /**
390 * @return array<ProviderInterface>
391 */
392 public function getApiProviders() {
393 return $this->apiProviders;
394 }
395
396 /**
397 * @param array $apiProviders
8882ff5c 398 * Array<ProviderInterface>.
70265090 399 * @return Kernel
82376c19
TO
400 */
401 public function setApiProviders($apiProviders) {
402 $this->apiProviders = $apiProviders;
70265090
TO
403 return $this;
404 }
405
406 /**
407 * @param ProviderInterface $apiProvider
8882ff5c 408 * The API provider responsible for executing the request.
70265090
TO
409 * @return Kernel
410 */
411 public function registerApiProvider($apiProvider) {
412 $this->apiProviders[] = $apiProvider;
413 if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) {
414 $this->getDispatcher()->addSubscriber($apiProvider);
415 }
416 return $this;
82376c19
TO
417 }
418
419 /**
420 * @return \Symfony\Component\EventDispatcher\EventDispatcher
421 */
422 public function getDispatcher() {
423 return $this->dispatcher;
424 }
425
426 /**
427 * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
8882ff5c 428 * The event dispatcher which receives kernel events.
70265090 429 * @return Kernel
82376c19
TO
430 */
431 public function setDispatcher($dispatcher) {
432 $this->dispatcher = $dispatcher;
70265090 433 return $this;
82376c19 434 }
96025800 435
6550386a 436}