CRM-14370 - Move helper functions into classes
[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'] = $this->parseVersion($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 /**
221 * We must be sure that every request uses only one version of the API.
222 *
223 * @param array $params
224 * @return int
225 */
226 protected function parseVersion($params) {
227 $desired_version = empty($params['version']) ? NULL : (int) $params['version'];
228 if (isset($desired_version) && is_integer($desired_version)) {
229 return $desired_version;
230 }
231 else {
232 // we will set the default to version 3 as soon as we find that it works.
233 return 3;
234 }
235 }
236
237 public function boot() {
238 require_once ('api/v3/utils.php');
239 require_once 'api/Exception.php';
240 _civicrm_api3_initialize();
241 }
242
243 /**
244 * Determine which, if any, service will execute the API request.
245 *
246 * @param $apiRequest
247 * @return array
248 * @throws \API_Exception
249 */
250 public function resolve($apiRequest) {
251 $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest));
252 $apiRequest = $resolveEvent->getApiRequest();
253 if (!$resolveEvent->getApiProvider()) {
254 throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
255 }
256 return array($resolveEvent->getApiProvider(), $apiRequest);
257 }
258
259 /**
260 * Determine if the API request is allowed (under current policy)
261 *
262 * @param ProviderInterface $apiProvider
263 * @param array $apiRequest
264 * @throws \API_Exception
265 */
266 public function authorize($apiProvider, $apiRequest) {
267 $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest));
268 if (!$event->isAuthorized()) {
269 throw new \Civi\API\Exception\UnauthorizedException("Authorization failed");
270 }
271 }
272
273 /**
274 * Allow third-party code to manipulate the API request before execution.
275 *
276 * @param ProviderInterface $apiProvider
277 * @param array $apiRequest
278 * @return mixed
279 */
280 public function prepare($apiProvider, $apiRequest) {
281 $event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest));
282 return $event->getApiRequest();
283 }
284
285 /**
286 * Allow third-party code to manipulate the API response after execution.
287 *
288 * @param ProviderInterface $apiProvider
289 * @param array $apiRequest
290 * @param array $result
291 * @return mixed
292 */
293 public function respond($apiProvider, $apiRequest, $result) {
294 $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result));
295 return $event->getResponse();
296 }
297
298 /**
299 * @param \Exception $e
300 * @param array $apiRequest
301 * @return array (API response)
302 */
303 public function formatException($e, $apiRequest) {
304 $data = array();
305 if (!empty($apiRequest['params']['debug'])) {
306 $data['trace'] = $e->getTraceAsString();
307 }
308 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
309 }
310
311 /**
312 * @param \API_Exception $e
313 * @param array $apiRequest
314 * @return array (API response)
315 */
316 public function formatApiException($e, $apiRequest) {
317 $data = $e->getExtraParams();
318 $data['entity'] = \CRM_Utils_Array::value('entity', $apiRequest);
319 $data['action'] = \CRM_Utils_Array::value('action', $apiRequest);
320
321 if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest))
322 && empty($data['trace']) // prevent recursion
323 ) {
324 $data['trace'] = $e->getTraceAsString();
325 }
326
327 return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
328 }
329
330 /**
331 * @param \PEAR_Exception $e
332 * @param array $apiRequest
333 * @return array (API response)
334 */
335 public function formatPearException($e, $apiRequest) {
336 $data = array();
337 $error = $e->getCause();
338 if ($error instanceof \DB_Error) {
339 $data["error_code"] = \DB::errorMessage($error->getCode());
340 $data["sql"] = $error->getDebugInfo();
341 }
342 if (!empty($apiRequest['params']['debug'])) {
343 if (method_exists($e, 'getUserInfo')) {
344 $data['debug_info'] = $error->getUserInfo();
345 }
346 if (method_exists($e, 'getExtraData')) {
347 $data['debug_info'] = $data + $error->getExtraData();
348 }
349 $data['trace'] = $e->getTraceAsString();
350 }
351 else {
352 $data['tip'] = "add debug=1 to your API call to have more info about the error";
353 }
354
355 return $this->createError($e->getMessage(), $data, $apiRequest);
356 }
357
358 /**
359 *
360 * @param <type> $data
361 * @param array $data
362 * @param object $apiRequest DAO / BAO object to be freed here
363 *
364 * @throws API_Exception
365 * @return array <type>
366 */
367 function createError($msg, $data, $apiRequest, $code = NULL) {
368 // FIXME what to do with $code?
369 if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') {
370 try {
371 $fields = _civicrm_api3_api_getfields($apiRequest);
372 _civicrm_api3_validate_fields($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields, TRUE);
373 } catch (Exception $e) {
374 $msg = $e->getMessage();
375 }
376 }
377
378 $data = civicrm_api3_create_error($msg, $data);
379
380 if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) {
381 $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code'];
382 throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data);
383 }
384
385 return $data;
386 }
387
388 /**
389 * @return mixed
390 */
391 public function formatResult($apiRequest, $result) {
392 if (isset($apiRequest, $apiRequest['params'])) {
393 if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) {
394 return (empty($result['is_error'])) ? 1 : 0;
395 }
396
397 if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) {
398 // FIXME dispatch
399 return $result['id'];
400 }
401 }
402 return $result;
403 }
404 }