CRM-20441 Return early if we have removed all Activity IDs as got no access to them
[civicrm-core.git] / Civi / API / Kernel.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
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 +--------------------------------------------------------------------+
26 */
27 namespace Civi\API;
28
29 use Civi\API\Event\AuthorizeEvent;
30 use Civi\API\Event\PrepareEvent;
31 use Civi\API\Event\ExceptionEvent;
32 use Civi\API\Event\ResolveEvent;
33 use Civi\API\Event\RespondEvent;
34 use Civi\API\Provider\ProviderInterface;
35
36 /**
37 * @package Civi
38 * @copyright CiviCRM LLC (c) 2004-2017
39 */
40 class Kernel {
41
42 /**
43 * @var \Symfony\Component\EventDispatcher\EventDispatcher
44 */
45 protected $dispatcher;
46
47 /**
48 * @var array<ProviderInterface>
49 */
50 protected $apiProviders;
51
52 /**
53 * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
54 * The event dispatcher which receives kernel events.
55 * @param array $apiProviders
56 * Array of ProviderInterface.
57 */
58 public function __construct($dispatcher, $apiProviders = array()) {
59 $this->apiProviders = $apiProviders;
60 $this->dispatcher = $dispatcher;
61 }
62
63 /**
64 * @deprecated
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.
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 *
84 * @param string $entity
85 * Type of entities to deal with.
86 * @param string $action
87 * Create, get, delete or some special action name.
88 * @param array $params
89 * Array to be passed to API function.
90 * @param mixed $extra
91 * Unused/deprecated.
92 *
93 * @return array|int
94 * @throws \API_Exception
95 */
96 public function runSafe($entity, $action, $params, $extra = NULL) {
97 $apiRequest = Request::create($entity, $action, $params, $extra);
98
99 try {
100 $apiResponse = $this->runRequest($apiRequest);
101 return $this->formatResult($apiRequest, $apiResponse);
102 }
103 catch (\Exception $e) {
104 $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, NULL, $apiRequest, $this));
105
106 if ($e instanceof \PEAR_Exception) {
107 $err = $this->formatPearException($e, $apiRequest);
108 }
109 elseif ($e instanceof \API_Exception) {
110 $err = $this->formatApiException($e, $apiRequest);
111 }
112 else {
113 $err = $this->formatException($e, $apiRequest);
114 }
115
116 return $this->formatResult($apiRequest, $err);
117 }
118 }
119
120 /**
121 * Determine if a hypothetical API call would be authorized.
122 *
123 * @param string $entity
124 * Type of entities to deal with.
125 * @param string $action
126 * Create, get, delete or some special action name.
127 * @param array $params
128 * Array to be passed to function.
129 * @param mixed $extra
130 * Unused/deprecated.
131 *
132 * @return bool
133 * TRUE if authorization would succeed.
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 {
141 $this->boot($apiRequest);
142 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
143 $this->authorize($apiProvider, $apiRequest);
144 return TRUE;
145 }
146 catch (\Civi\API\Exception\UnauthorizedException $e) {
147 return FALSE;
148 }
149 }
150
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) {
163 $this->boot($apiRequest);
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
171 return $this->respond($apiProvider, $apiRequest, $result);
172 }
173
174 /**
175 * Bootstrap - Load basic dependencies and sanity-check inputs.
176 *
177 * @param \Civi\API\V4\Action|array $apiRequest
178 * @throws \API_Exception
179 */
180 public function boot($apiRequest) {
181 require_once 'api/Exception.php';
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 }
200 }
201
202 /**
203 * @param $apiRequest
204 * @throws \API_Exception
205 */
206 protected function validate($apiRequest) {
207 }
208
209 /**
210 * Determine which, if any, service will execute the API request.
211 *
212 * @param array $apiRequest
213 * The full description of the API request.
214 * @throws Exception\NotImplementedException
215 * @return array
216 * Array(0 => ProviderInterface, 1 => array).
217 */
218 public function resolve($apiRequest) {
219 /** @var ResolveEvent $resolveEvent */
220 $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest, $this));
221 $apiRequest = $resolveEvent->getApiRequest();
222 if (!$resolveEvent->getApiProvider()) {
223 throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
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
232 * The API provider responsible for executing the request.
233 * @param array $apiRequest
234 * The full description of the API request.
235 * @throws Exception\UnauthorizedException
236 */
237 public function authorize($apiProvider, $apiRequest) {
238 /** @var AuthorizeEvent $event */
239 $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest, $this));
240 if (!$event->isAuthorized()) {
241 throw new \Civi\API\Exception\UnauthorizedException("Authorization failed");
242 }
243 }
244
245 /**
246 * Allow third-party code to manipulate the API request before execution.
247 *
248 * @param ProviderInterface $apiProvider
249 * The API provider responsible for executing the request.
250 * @param array $apiRequest
251 * The full description of the API request.
252 * @return mixed
253 */
254 public function prepare($apiProvider, $apiRequest) {
255 /** @var PrepareEvent $event */
256 $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest, $this));
257 return $event->getApiRequest();
258 }
259
260 /**
261 * Allow third-party code to manipulate the API response after execution.
262 *
263 * @param ProviderInterface $apiProvider
264 * The API provider responsible for executing the request.
265 * @param array $apiRequest
266 * The full description of the API request.
267 * @param array $result
268 * The response to return to the client.
269 * @return mixed
270 */
271 public function respond($apiProvider, $apiRequest, $result) {
272 /** @var RespondEvent $event */
273 $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result, $this));
274 return $event->getResponse();
275 }
276
277 /**
278 * @param int $version
279 * API version.
280 * @return array
281 * Array<string>.
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) {
287 /** @var ProviderInterface $provider */
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
297 * API version.
298 * @param string $entity
299 * API entity.
300 * @return array
301 * Array<string>
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) {
307 /** @var ProviderInterface $provider */
308 $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity));
309 }
310 $actionNames = array_unique($actionNames);
311 sort($actionNames);
312 return $actionNames;
313 }
314
315 /**
316 * @param \Exception $e
317 * An unhandled exception.
318 * @param array $apiRequest
319 * The full description of the API request.
320 * @return array
321 * API response.
322 */
323 public function formatException($e, $apiRequest) {
324 $data = array();
325 if (!empty($apiRequest['params']['debug'])) {
326 $data['trace'] = $e->getTraceAsString();
327 }
328 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
329 }
330
331 /**
332 * @param \API_Exception $e
333 * An unhandled exception.
334 * @param array $apiRequest
335 * The full description of the API request.
336 * @return array
337 * (API response)
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
350 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
351 }
352
353 /**
354 * @param \PEAR_Exception $e
355 * An unhandled exception.
356 * @param array $apiRequest
357 * The full description of the API request.
358 * @return array
359 * API response.
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
381 return $this->createError($e->getMessage(), $data, $apiRequest);
382 }
383
384 /**
385 * @param string $msg
386 * Descriptive error message.
387 * @param array $data
388 * Error data.
389 * @param array $apiRequest
390 * The full description of the API request.
391 * @param mixed $code
392 * Doesn't appear to be used.
393 *
394 * @throws \API_Exception
395 * @return array
396 * Array<type>.
397 */
398 public function createError($msg, $data, $apiRequest, $code = NULL) {
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);
403 _civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields);
404 }
405 catch (\Exception $e) {
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;
418 }
419
420 /**
421 * @param array $apiRequest
422 * The full description of the API request.
423 * @param array $result
424 * The response to return to the client.
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 }
440
441 /**
442 * @return array<ProviderInterface>
443 */
444 public function getApiProviders() {
445 return $this->apiProviders;
446 }
447
448 /**
449 * @param array $apiProviders
450 * Array<ProviderInterface>.
451 * @return Kernel
452 */
453 public function setApiProviders($apiProviders) {
454 $this->apiProviders = $apiProviders;
455 return $this;
456 }
457
458 /**
459 * @param ProviderInterface $apiProvider
460 * The API provider responsible for executing the request.
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;
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
480 * The event dispatcher which receives kernel events.
481 * @return Kernel
482 */
483 public function setDispatcher($dispatcher) {
484 $this->dispatcher = $dispatcher;
485 return $this;
486 }
487
488 }