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 | ||
6550386a EM |
54 | /** |
55 | * @param $dispatcher | |
56 | * @param array $apiProviders | |
57 | */ | |
0f643fb2 TO |
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 | */ | |
70265090 | 74 | public function run($entity, $action, $params, $extra = NULL) { |
787604ff TO |
75 | /** |
76 | * @var $apiProvider \Civi\API\Provider\ProviderInterface|NULL | |
77 | */ | |
78 | $apiProvider = NULL; | |
79 | ||
70be69e2 TO |
80 | // TODO Define alternative calling convention makes it easier to construct $apiRequest |
81 | // without the ambiguity of "data" vs "options" | |
d3159a21 | 82 | $apiRequest = Request::create($entity, $action, $params, $extra); |
0f643fb2 | 83 | |
0f643fb2 | 84 | try { |
0f643fb2 TO |
85 | if (!is_array($params)) { |
86 | throw new \API_Exception('Input variable `params` is not an array', 2000); | |
87 | } | |
132ec342 TO |
88 | |
89 | $this->boot(); | |
0f643fb2 | 90 | $errorScope = \CRM_Core_TemporaryErrorScope::useException(); |
132ec342 | 91 | |
5fda6437 TO |
92 | list($apiProvider, $apiRequest) = $this->resolve($apiRequest); |
93 | $this->authorize($apiProvider, $apiRequest); | |
94 | $apiRequest = $this->prepare($apiProvider, $apiRequest); | |
787604ff TO |
95 | $result = $apiProvider->invoke($apiRequest); |
96 | ||
5fda6437 TO |
97 | $apiResponse = $this->respond($apiProvider, $apiRequest, $result); |
98 | return $this->formatResult($apiRequest, $apiResponse); | |
0f643fb2 TO |
99 | } |
100 | catch (\Exception $e) { | |
787604ff | 101 | $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, $apiProvider, $apiRequest)); |
91798b7a | 102 | |
91798b7a TO |
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); | |
0f643fb2 | 109 | } |
91798b7a | 110 | |
143ed725 | 111 | return $this->formatResult($apiRequest, $err); |
0f643fb2 | 112 | } |
c65db512 TO |
113 | } |
114 | ||
132ec342 TO |
115 | public function boot() { |
116 | require_once ('api/v3/utils.php'); | |
117 | require_once 'api/Exception.php'; | |
118 | _civicrm_api3_initialize(); | |
119 | } | |
91798b7a | 120 | |
5fda6437 TO |
121 | /** |
122 | * Determine which, if any, service will execute the API request. | |
123 | * | |
446f0940 TO |
124 | * @param array $apiRequest |
125 | * @throws Exception\NotImplementedException | |
5fda6437 | 126 | * @return array |
5fda6437 TO |
127 | */ |
128 | public function resolve($apiRequest) { | |
446f0940 | 129 | /** @var ResolveEvent $resolveEvent */ |
5fda6437 TO |
130 | $resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest)); |
131 | $apiRequest = $resolveEvent->getApiRequest(); | |
132 | if (!$resolveEvent->getApiProvider()) { | |
fedf821c | 133 | throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)"); |
5fda6437 TO |
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 | |
446f0940 | 143 | * @throws Exception\UnauthorizedException |
5fda6437 TO |
144 | */ |
145 | public function authorize($apiProvider, $apiRequest) { | |
446f0940 | 146 | /** @var AuthorizeEvent $event */ |
5fda6437 TO |
147 | $event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest)); |
148 | if (!$event->isAuthorized()) { | |
fedf821c | 149 | throw new \Civi\API\Exception\UnauthorizedException("Authorization failed"); |
5fda6437 TO |
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) { | |
446f0940 | 161 | /** @var PrepareEvent $event */ |
5fda6437 TO |
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) { | |
446f0940 | 175 | /** @var RespondEvent $event */ |
5fda6437 TO |
176 | $event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result)); |
177 | return $event->getResponse(); | |
178 | } | |
179 | ||
82376c19 TO |
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) { | |
446f0940 | 188 | /** @var ProviderInterface $provider */ |
82376c19 TO |
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) { | |
446f0940 | 205 | /** @var ProviderInterface $provider */ |
82376c19 TO |
206 | $actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity)); |
207 | } | |
208 | $actionNames = array_unique($actionNames); | |
209 | sort($actionNames); | |
210 | return $actionNames; | |
211 | } | |
212 | ||
91798b7a TO |
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 | } | |
9c465c3b | 223 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
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 | ||
9c465c3b | 242 | return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode()); |
91798b7a TO |
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 | ||
9c465c3b TO |
270 | return $this->createError($e->getMessage(), $data, $apiRequest); |
271 | } | |
272 | ||
273 | /** | |
274 | * | |
446f0940 | 275 | * @param string $msg |
9c465c3b | 276 | * @param array $data |
446f0940 TO |
277 | * @param array $apiRequest |
278 | * @param mixed $code doesn't appear to be used | |
9c465c3b | 279 | * |
446f0940 | 280 | * @throws \API_Exception |
9c465c3b TO |
281 | * @return array <type> |
282 | */ | |
283 | function createError($msg, $data, $apiRequest, $code = NULL) { | |
284 | // FIXME what to do with $code? | |
285 | if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') { | |
286 | try { | |
287 | $fields = _civicrm_api3_api_getfields($apiRequest); | |
288 | _civicrm_api3_validate_fields($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields, TRUE); | |
446f0940 | 289 | } catch (\Exception $e) { |
9c465c3b TO |
290 | $msg = $e->getMessage(); |
291 | } | |
292 | } | |
293 | ||
294 | $data = civicrm_api3_create_error($msg, $data); | |
295 | ||
296 | if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) { | |
297 | $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code']; | |
298 | throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data); | |
299 | } | |
300 | ||
301 | return $data; | |
91798b7a | 302 | } |
143ed725 TO |
303 | |
304 | /** | |
446f0940 TO |
305 | * @param array $apiRequest |
306 | * @param array $result | |
143ed725 TO |
307 | * @return mixed |
308 | */ | |
309 | public function formatResult($apiRequest, $result) { | |
310 | if (isset($apiRequest, $apiRequest['params'])) { | |
311 | if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) { | |
312 | return (empty($result['is_error'])) ? 1 : 0; | |
313 | } | |
314 | ||
315 | if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) { | |
316 | // FIXME dispatch | |
317 | return $result['id']; | |
318 | } | |
319 | } | |
320 | return $result; | |
321 | } | |
82376c19 TO |
322 | |
323 | /** | |
324 | * @return array<ProviderInterface> | |
325 | */ | |
326 | public function getApiProviders() { | |
327 | return $this->apiProviders; | |
328 | } | |
329 | ||
330 | /** | |
331 | * @param array $apiProviders | |
70265090 | 332 | * @return Kernel |
82376c19 TO |
333 | */ |
334 | public function setApiProviders($apiProviders) { | |
335 | $this->apiProviders = $apiProviders; | |
70265090 TO |
336 | return $this; |
337 | } | |
338 | ||
339 | /** | |
340 | * @param ProviderInterface $apiProvider | |
341 | * @return Kernel | |
342 | */ | |
343 | public function registerApiProvider($apiProvider) { | |
344 | $this->apiProviders[] = $apiProvider; | |
345 | if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) { | |
346 | $this->getDispatcher()->addSubscriber($apiProvider); | |
347 | } | |
348 | return $this; | |
82376c19 TO |
349 | } |
350 | ||
351 | /** | |
352 | * @return \Symfony\Component\EventDispatcher\EventDispatcher | |
353 | */ | |
354 | public function getDispatcher() { | |
355 | return $this->dispatcher; | |
356 | } | |
357 | ||
358 | /** | |
359 | * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher | |
70265090 | 360 | * @return Kernel |
82376c19 TO |
361 | */ |
362 | public function setDispatcher($dispatcher) { | |
363 | $this->dispatcher = $dispatcher; | |
70265090 | 364 | return $this; |
82376c19 | 365 | } |
6550386a | 366 | } |