CRM-14370 - API Kernel - Move dispatch into helpers
[civicrm-core.git] / Civi / API / Kernel.php
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 */
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 *
38 * @package Civi
39 * @copyright CiviCRM LLC (c) 2004-2013
40 */
41
42 class Kernel {
43
44 /**
45 * @var \Symfony\Component\EventDispatcher\EventDispatcher
46 */
47 protected $dispatcher;
48
49 /**
50 * @var array<ProviderInterface>
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 */
70 public function run($entity, $action, $params, $extra) {
71 /**
72 * @var $apiProvider \Civi\API\Provider\ProviderInterface|NULL
73 */
74 $apiProvider = NULL;
75
76 // TODO Define alternative calling convention makes it easier to construct $apiRequest
77 // without the ambiguity of "data" vs "options"
78 $apiRequest = $this->createRequest($entity, $action, $params, $extra);
79
80 try {
81 if (!is_array($params)) {
82 throw new \API_Exception('Input variable `params` is not an array', 2000);
83 }
84
85 $this->boot();
86 $errorScope = \CRM_Core_TemporaryErrorScope::useException();
87
88 list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
89 $this->authorize($apiProvider, $apiRequest);
90 $apiRequest = $this->prepare($apiProvider, $apiRequest);
91 $result = $apiProvider->invoke($apiRequest);
92
93 if (\CRM_Utils_Array::value('is_error', $result, 0) == 0) {
94 _civicrm_api_call_nested_api($apiRequest['params'], $result, $apiRequest['action'], $apiRequest['entity'], $apiRequest['version']);
95 }
96
97 $apiResponse = $this->respond($apiProvider, $apiRequest, $result);
98 return $this->formatResult($apiRequest, $apiResponse);
99 }
100 catch (\Exception $e) {
101 $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, $apiProvider, $apiRequest));
102
103 if ($e instanceof \PEAR_Exception) {
104 $err = $this->formatPearException($e, $apiRequest);
105 } elseif ($e instanceof \API_Exception) {
106 $err = $this->formatApiException($e, $apiRequest);
107 } else {
108 $err = $this->formatException($e, $apiRequest);
109 }
110
111 return $this->formatResult($apiRequest, $err);
112 }
113
114 }
115
116 /**
117 * Create a formatted/normalized request object.
118 *
119 * @param string $entity
120 * @param string $action
121 * @param array $params
122 * @param mixed $extra
123 * @return array the request descriptor; keys:
124 * - version: int
125 * - entity: string
126 * - action: string
127 * - params: array (string $key => mixed $value) [deprecated in v4]
128 * - extra: unspecified
129 * - fields: NULL|array (string $key => array $fieldSpec)
130 * - options: \CRM_Utils_OptionBag derived from params [v4-only]
131 * - data: \CRM_Utils_OptionBag derived from params [v4-only]
132 * - chains: unspecified derived from params [v4-only]
133 */
134 public function createRequest($entity, $action, $params, $extra) {
135 $apiRequest = array(); // new \Civi\API\Request();
136 $apiRequest['version'] = civicrm_get_api_version($params);
137 $apiRequest['params'] = $params;
138 $apiRequest['extra'] = $extra;
139 $apiRequest['fields'] = NULL;
140
141 if ($apiRequest['version'] <= 3) {
142 // APIv1-v3 munges entity/action names, which means that the same name can be written
143 // multiple ways. That makes it harder to work with.
144 $apiRequest['entity'] = \CRM_Utils_String::munge($entity);
145 $apiRequest['action'] = \CRM_Utils_String::munge($action);
146 }
147 else {
148 // APIv4 requires exact entity/action name; deviations should cause errors
149 if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $entity)) {
150 throw new \API_Exception("Malformed entity");
151 }
152 if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $action)) {
153 throw new \API_Exception("Malformed action");
154 }
155 $apiRequest['entity'] = $entity;
156 $apiRequest['action'] = $action;
157 }
158
159 // APIv1-v3 mix data+options in $params which means that each API callback is responsible
160 // for splitting the two. In APIv4, the split is done systematically so that we don't
161 // so much parsing logic spread around.
162 if ($apiRequest['version'] >= 4) {
163 $options = array();
164 $data = array();
165 $chains = array();
166 foreach ($params as $key => $value) {
167 if ($key == 'options') {
168 $options = array_merge($options, $value);
169 }
170 elseif ($key == 'return') {
171 if (!isset($options['return'])) {
172 $options['return'] = array();
173 }
174 $options['return'] = array_merge($options['return'], $value);
175 }
176 elseif (preg_match('/^option\.(.*)$/', $key, $matches)) {
177 $options[$matches[1]] = $value;
178 }
179 elseif (preg_match('/^return\.(.*)$/', $key, $matches)) {
180 if ($value) {
181 if (!isset($options['return'])) {
182 $options['return'] = array();
183 }
184 $options['return'][] = $matches[1];
185 }
186 }
187 elseif (preg_match('/^format\.(.*)$/', $key, $matches)) {
188 if ($value) {
189 if (!isset($options['format'])) {
190 $options['format'] = $matches[1];
191 }
192 else {
193 throw new \API_Exception("Too many API formats specified");
194 }
195 }
196 }
197 elseif (preg_match('/^api\./', $key)) {
198 // FIXME: represent subrequests as instances of "Request"
199 $chains[$key] = $value;
200 }
201 elseif ($key == 'debug') {
202 $options['debug'] = $value;
203 }
204 elseif ($key == 'version') {
205 // ignore
206 }
207 else {
208 $data[$key] = $value;
209
210 }
211 }
212 $apiRequest['options'] = new \CRM_Utils_OptionBag($options);
213 $apiRequest['data'] = new \CRM_Utils_OptionBag($data);
214 $apiRequest['chains'] = $chains;
215 }
216
217 return $apiRequest;
218 }
219
220 public function boot() {
221 require_once ('api/v3/utils.php');
222 require_once 'api/Exception.php';
223 _civicrm_api3_initialize();
224 }
225
226 /**
227 * Determine which, if any, service will execute the API request.
228 *
229 * @param $apiRequest
230 * @return array
231 * @throws \API_Exception
232 */
233 public function resolve($apiRequest) {
234 $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest));
235 $apiRequest = $resolveEvent->getApiRequest();
236 if (!$resolveEvent->getApiProvider()) {
237 throw new \API_Exception("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)", \API_Exception::NOT_IMPLEMENTED);
238 }
239 return array($resolveEvent->getApiProvider(), $apiRequest);
240 }
241
242 /**
243 * Determine if the API request is allowed (under current policy)
244 *
245 * @param ProviderInterface $apiProvider
246 * @param array $apiRequest
247 * @throws \API_Exception
248 */
249 public function authorize($apiProvider, $apiRequest) {
250 $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest));
251 if (!$event->isAuthorized()) {
252 throw new \API_Exception("Authorization failed", \API_Exception::UNAUTHORIZED);
253 }
254 }
255
256 /**
257 * Allow third-party code to manipulate the API request before execution.
258 *
259 * @param ProviderInterface $apiProvider
260 * @param array $apiRequest
261 * @return mixed
262 */
263 public function prepare($apiProvider, $apiRequest) {
264 $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest));
265 return $event->getApiRequest();
266 }
267
268 /**
269 * Allow third-party code to manipulate the API response after execution.
270 *
271 * @param ProviderInterface $apiProvider
272 * @param array $apiRequest
273 * @param array $result
274 * @return mixed
275 */
276 public function respond($apiProvider, $apiRequest, $result) {
277 $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result));
278 return $event->getResponse();
279 }
280
281 /**
282 * @param \Exception $e
283 * @param array $apiRequest
284 * @return array (API response)
285 */
286 public function formatException($e, $apiRequest) {
287 $data = array();
288 if (!empty($apiRequest['params']['debug'])) {
289 $data['trace'] = $e->getTraceAsString();
290 }
291 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
292 }
293
294 /**
295 * @param \API_Exception $e
296 * @param array $apiRequest
297 * @return array (API response)
298 */
299 public function formatApiException($e, $apiRequest) {
300 $data = $e->getExtraParams();
301 $data['entity'] = \CRM_Utils_Array::value('entity', $apiRequest);
302 $data['action'] = \CRM_Utils_Array::value('action', $apiRequest);
303
304 if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest))
305 && empty($data['trace']) // prevent recursion
306 ) {
307 $data['trace'] = $e->getTraceAsString();
308 }
309
310 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
311 }
312
313 /**
314 * @param \PEAR_Exception $e
315 * @param array $apiRequest
316 * @return array (API response)
317 */
318 public function formatPearException($e, $apiRequest) {
319 $data = array();
320 $error = $e->getCause();
321 if ($error instanceof \DB_Error) {
322 $data["error_code"] = \DB::errorMessage($error->getCode());
323 $data["sql"] = $error->getDebugInfo();
324 }
325 if (!empty($apiRequest['params']['debug'])) {
326 if (method_exists($e, 'getUserInfo')) {
327 $data['debug_info'] = $error->getUserInfo();
328 }
329 if (method_exists($e, 'getExtraData')) {
330 $data['debug_info'] = $data + $error->getExtraData();
331 }
332 $data['trace'] = $e->getTraceAsString();
333 }
334 else {
335 $data['tip'] = "add debug=1 to your API call to have more info about the error";
336 }
337
338 return $this->createError($e->getMessage(), $data, $apiRequest);
339 }
340
341 /**
342 *
343 * @param <type> $data
344 * @param array $data
345 * @param object $apiRequest DAO / BAO object to be freed here
346 *
347 * @throws API_Exception
348 * @return array <type>
349 */
350 function createError($msg, $data, $apiRequest, $code = NULL) {
351 // FIXME what to do with $code?
352 if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') {
353 try {
354 $fields = _civicrm_api3_api_getfields($apiRequest);
355 _civicrm_api3_validate_fields($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields, TRUE);
356 } catch (Exception $e) {
357 $msg = $e->getMessage();
358 }
359 }
360
361 $data = civicrm_api3_create_error($msg, $data);
362
363 if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) {
364 $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code'];
365 throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data);
366 }
367
368 return $data;
369 }
370
371 /**
372 * @return mixed
373 */
374 public function formatResult($apiRequest, $result) {
375 if (isset($apiRequest, $apiRequest['params'])) {
376 if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) {
377 return (empty($result['is_error'])) ? 1 : 0;
378 }
379
380 if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) {
381 // FIXME dispatch
382 return $result['id'];
383 }
384 }
385 return $result;
386 }
387 }