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