Commit | Line | Data |
---|---|---|
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 | */ | |
27 | namespace Civi\API; | |
28 | ||
132ec342 TO |
29 | use Civi\API\Event\AuthorizeEvent; |
30 | use Civi\API\Event\PrepareEvent; | |
31 | use Civi\API\Event\ExceptionEvent; | |
787604ff | 32 | use Civi\API\Event\ResolveEvent; |
132ec342 | 33 | use Civi\API\Event\RespondEvent; |
5fda6437 | 34 | use Civi\API\Provider\ProviderInterface; |
132ec342 | 35 | |
0f643fb2 TO |
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 | /** | |
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 | } |