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