Merge remote-tracking branch 'upstream/4.4' into 4.4-master-2014-04-28-11-04-58
[civicrm-core.git] / Civi / API / Kernel.php
CommitLineData
0f643fb2
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.4 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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*/
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
TO
36/**
37 *
38 * @package Civi
39 * @copyright CiviCRM LLC (c) 2004-2013
40 */
41
42class Kernel {
43
44 /**
45 * @var \Symfony\Component\EventDispatcher\EventDispatcher
46 */
47 protected $dispatcher;
48
49 /**
5fda6437 50 * @var array<ProviderInterface>
0f643fb2
TO
51 */
52 protected $apiProviders;
53
54 function __construct($dispatcher, $apiProviders = array()) {
55 $this->apiProviders = $apiProviders;
56 $this->dispatcher = $dispatcher;
57 }
58
59 /**
60 * @param string $entity
61 * type of entities to deal with
62 * @param string $action
63 * create, get, delete or some special action name.
64 * @param array $params
65 * array to be passed to function
66 * @param null $extra
67 *
68 * @return array|int
69 */
70265090 70 public function run($entity, $action, $params, $extra = NULL) {
787604ff
TO
71 /**
72 * @var $apiProvider \Civi\API\Provider\ProviderInterface|NULL
73 */
74 $apiProvider = NULL;
75
70be69e2
TO
76 // TODO Define alternative calling convention makes it easier to construct $apiRequest
77 // without the ambiguity of "data" vs "options"
d3159a21 78 $apiRequest = Request::create($entity, $action, $params, $extra);
0f643fb2 79
0f643fb2 80 try {
0f643fb2
TO
81 if (!is_array($params)) {
82 throw new \API_Exception('Input variable `params` is not an array', 2000);
83 }
132ec342
TO
84
85 $this->boot();
0f643fb2 86 $errorScope = \CRM_Core_TemporaryErrorScope::useException();
132ec342 87
5fda6437
TO
88 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
89 $this->authorize($apiProvider, $apiRequest);
90 $apiRequest = $this->prepare($apiProvider, $apiRequest);
787604ff
TO
91 $result = $apiProvider->invoke($apiRequest);
92
5fda6437
TO
93 $apiResponse = $this->respond($apiProvider, $apiRequest, $result);
94 return $this->formatResult($apiRequest, $apiResponse);
0f643fb2
TO
95 }
96 catch (\Exception $e) {
787604ff 97 $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, $apiProvider, $apiRequest));
91798b7a 98
91798b7a
TO
99 if ($e instanceof \PEAR_Exception) {
100 $err = $this->formatPearException($e, $apiRequest);
101 } elseif ($e instanceof \API_Exception) {
102 $err = $this->formatApiException($e, $apiRequest);
103 } else {
104 $err = $this->formatException($e, $apiRequest);
0f643fb2 105 }
91798b7a 106
143ed725 107 return $this->formatResult($apiRequest, $err);
0f643fb2 108 }
c65db512
TO
109 }
110
132ec342
TO
111 public function boot() {
112 require_once ('api/v3/utils.php');
113 require_once 'api/Exception.php';
114 _civicrm_api3_initialize();
115 }
91798b7a 116
5fda6437
TO
117 /**
118 * Determine which, if any, service will execute the API request.
119 *
446f0940
TO
120 * @param array $apiRequest
121 * @throws Exception\NotImplementedException
5fda6437 122 * @return array
5fda6437
TO
123 */
124 public function resolve($apiRequest) {
446f0940 125 /** @var ResolveEvent $resolveEvent */
5fda6437
TO
126 $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest));
127 $apiRequest = $resolveEvent->getApiRequest();
128 if (!$resolveEvent->getApiProvider()) {
fedf821c 129 throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
5fda6437
TO
130 }
131 return array($resolveEvent->getApiProvider(), $apiRequest);
132 }
133
134 /**
135 * Determine if the API request is allowed (under current policy)
136 *
137 * @param ProviderInterface $apiProvider
138 * @param array $apiRequest
446f0940 139 * @throws Exception\UnauthorizedException
5fda6437
TO
140 */
141 public function authorize($apiProvider, $apiRequest) {
446f0940 142 /** @var AuthorizeEvent $event */
5fda6437
TO
143 $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest));
144 if (!$event->isAuthorized()) {
fedf821c 145 throw new \Civi\API\Exception\UnauthorizedException("Authorization failed");
5fda6437
TO
146 }
147 }
148
149 /**
150 * Allow third-party code to manipulate the API request before execution.
151 *
152 * @param ProviderInterface $apiProvider
153 * @param array $apiRequest
154 * @return mixed
155 */
156 public function prepare($apiProvider, $apiRequest) {
446f0940 157 /** @var PrepareEvent $event */
5fda6437
TO
158 $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest));
159 return $event->getApiRequest();
160 }
161
162 /**
163 * Allow third-party code to manipulate the API response after execution.
164 *
165 * @param ProviderInterface $apiProvider
166 * @param array $apiRequest
167 * @param array $result
168 * @return mixed
169 */
170 public function respond($apiProvider, $apiRequest, $result) {
446f0940 171 /** @var RespondEvent $event */
5fda6437
TO
172 $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result));
173 return $event->getResponse();
174 }
175
82376c19
TO
176 /**
177 * @param int $version
178 * @return array<string>
179 */
180 public function getEntityNames($version) {
181 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
182 $entityNames = array();
183 foreach ($this->getApiProviders() as $provider) {
446f0940 184 /** @var ProviderInterface $provider */
82376c19
TO
185 $entityNames = array_merge($entityNames, $provider->getEntityNames($version));
186 }
187 $entityNames = array_unique($entityNames);
188 sort($entityNames);
189 return $entityNames;
190 }
191
192 /**
193 * @param int $version
194 * @param string $entity
195 * @return array<string>
196 */
197 public function getActionNames($version, $entity) {
198 // Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
199 $actionNames = array();
200 foreach ($this->getApiProviders() as $provider) {
446f0940 201 /** @var ProviderInterface $provider */
82376c19
TO
202 $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity));
203 }
204 $actionNames = array_unique($actionNames);
205 sort($actionNames);
206 return $actionNames;
207 }
208
91798b7a
TO
209 /**
210 * @param \Exception $e
211 * @param array $apiRequest
212 * @return array (API response)
213 */
214 public function formatException($e, $apiRequest) {
215 $data = array();
216 if (!empty($apiRequest['params']['debug'])) {
217 $data['trace'] = $e->getTraceAsString();
218 }
9c465c3b 219 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
91798b7a
TO
220 }
221
222 /**
223 * @param \API_Exception $e
224 * @param array $apiRequest
225 * @return array (API response)
226 */
227 public function formatApiException($e, $apiRequest) {
228 $data = $e->getExtraParams();
229 $data['entity'] = \CRM_Utils_Array::value('entity', $apiRequest);
230 $data['action'] = \CRM_Utils_Array::value('action', $apiRequest);
231
232 if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest))
233 && empty($data['trace']) // prevent recursion
234 ) {
235 $data['trace'] = $e->getTraceAsString();
236 }
237
9c465c3b 238 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
91798b7a
TO
239 }
240
241 /**
242 * @param \PEAR_Exception $e
243 * @param array $apiRequest
244 * @return array (API response)
245 */
246 public function formatPearException($e, $apiRequest) {
247 $data = array();
248 $error = $e->getCause();
249 if ($error instanceof \DB_Error) {
250 $data["error_code"] = \DB::errorMessage($error->getCode());
251 $data["sql"] = $error->getDebugInfo();
252 }
253 if (!empty($apiRequest['params']['debug'])) {
254 if (method_exists($e, 'getUserInfo')) {
255 $data['debug_info'] = $error->getUserInfo();
256 }
257 if (method_exists($e, 'getExtraData')) {
258 $data['debug_info'] = $data + $error->getExtraData();
259 }
260 $data['trace'] = $e->getTraceAsString();
261 }
262 else {
263 $data['tip'] = "add debug=1 to your API call to have more info about the error";
264 }
265
9c465c3b
TO
266 return $this->createError($e->getMessage(), $data, $apiRequest);
267 }
268
269 /**
270 *
446f0940 271 * @param string $msg
9c465c3b 272 * @param array $data
446f0940
TO
273 * @param array $apiRequest
274 * @param mixed $code doesn't appear to be used
9c465c3b 275 *
446f0940 276 * @throws \API_Exception
9c465c3b
TO
277 * @return array <type>
278 */
279 function createError($msg, $data, $apiRequest, $code = NULL) {
280 // FIXME what to do with $code?
281 if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') {
282 try {
283 $fields = _civicrm_api3_api_getfields($apiRequest);
284 _civicrm_api3_validate_fields($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields, TRUE);
446f0940 285 } catch (\Exception $e) {
9c465c3b
TO
286 $msg = $e->getMessage();
287 }
288 }
289
290 $data = civicrm_api3_create_error($msg, $data);
291
292 if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) {
293 $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code'];
294 throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data);
295 }
296
297 return $data;
91798b7a 298 }
143ed725
TO
299
300 /**
446f0940
TO
301 * @param array $apiRequest
302 * @param array $result
143ed725
TO
303 * @return mixed
304 */
305 public function formatResult($apiRequest, $result) {
306 if (isset($apiRequest, $apiRequest['params'])) {
307 if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) {
308 return (empty($result['is_error'])) ? 1 : 0;
309 }
310
311 if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) {
312 // FIXME dispatch
313 return $result['id'];
314 }
315 }
316 return $result;
317 }
82376c19
TO
318
319 /**
320 * @return array<ProviderInterface>
321 */
322 public function getApiProviders() {
323 return $this->apiProviders;
324 }
325
326 /**
327 * @param array $apiProviders
70265090 328 * @return Kernel
82376c19
TO
329 */
330 public function setApiProviders($apiProviders) {
331 $this->apiProviders = $apiProviders;
70265090
TO
332 return $this;
333 }
334
335 /**
336 * @param ProviderInterface $apiProvider
337 * @return Kernel
338 */
339 public function registerApiProvider($apiProvider) {
340 $this->apiProviders[] = $apiProvider;
341 if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) {
342 $this->getDispatcher()->addSubscriber($apiProvider);
343 }
344 return $this;
82376c19
TO
345 }
346
347 /**
348 * @return \Symfony\Component\EventDispatcher\EventDispatcher
349 */
350 public function getDispatcher() {
351 return $this->dispatcher;
352 }
353
354 /**
355 * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
70265090 356 * @return Kernel
82376c19
TO
357 */
358 public function setDispatcher($dispatcher) {
359 $this->dispatcher = $dispatcher;
70265090 360 return $this;
82376c19 361 }
0f643fb2 362}