Commit | Line | Data |
---|---|---|
787604ff TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
fee14197 | 4 | | CiviCRM version 5 | |
787604ff | 5 | +--------------------------------------------------------------------+ |
6b83d5bd | 6 | | Copyright CiviCRM LLC (c) 2004-2019 | |
787604ff TO |
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 | +--------------------------------------------------------------------+ | |
d25dd0ee | 26 | */ |
787604ff TO |
27 | |
28 | namespace Civi\API\Provider; | |
8882ff5c | 29 | |
787604ff TO |
30 | use Civi\API\Events; |
31 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
32 | ||
33 | /** | |
34 | * This class manages the loading of API's using strict file+function naming | |
35 | * conventions. | |
36 | */ | |
37 | class MagicFunctionProvider implements EventSubscriberInterface, ProviderInterface { | |
6550386a EM |
38 | /** |
39 | * @return array | |
40 | */ | |
787604ff TO |
41 | public static function getSubscribedEvents() { |
42 | return array( | |
43 | Events::RESOLVE => array( | |
44 | array('onApiResolve', Events::W_MIDDLE), | |
45 | ), | |
46 | ); | |
47 | } | |
48 | ||
c65db512 TO |
49 | /** |
50 | * @var array (string $cachekey => array('function' => string, 'is_generic' => bool)) | |
51 | */ | |
52 | private $cache; | |
53 | ||
6550386a | 54 | /** |
6550386a | 55 | */ |
8882ff5c | 56 | public function __construct() { |
c65db512 TO |
57 | $this->cache = array(); |
58 | } | |
59 | ||
6550386a EM |
60 | /** |
61 | * @param \Civi\API\Event\ResolveEvent $event | |
8882ff5c | 62 | * API resolution event. |
6550386a | 63 | */ |
787604ff TO |
64 | public function onApiResolve(\Civi\API\Event\ResolveEvent $event) { |
65 | $apiRequest = $event->getApiRequest(); | |
c65db512 | 66 | $resolved = $this->resolve($apiRequest); |
787604ff TO |
67 | if ($resolved['function']) { |
68 | $apiRequest += $resolved; | |
69 | $event->setApiRequest($apiRequest); | |
70 | $event->setApiProvider($this); | |
71 | $event->stopPropagation(); | |
72 | } | |
73 | } | |
74 | ||
82376c19 | 75 | /** |
3dde2c6c | 76 | * @inheritDoc |
257e7666 EM |
77 | * @param array $apiRequest |
78 | * @return array | |
82376c19 | 79 | */ |
787604ff TO |
80 | public function invoke($apiRequest) { |
81 | $function = $apiRequest['function']; | |
82 | if ($apiRequest['function'] && $apiRequest['is_generic']) { | |
83 | // Unlike normal API implementations, generic implementations require explicit | |
84 | // knowledge of the entity and action (as well as $params). Bundle up these bits | |
85 | // into a convenient data structure. | |
d6f190fb | 86 | if ($apiRequest['action'] === 'getsingle') { |
87 | // strip any api nested parts here as otherwise chaining may happen twice | |
88 | // see https://lab.civicrm.org/dev/core/issues/643 | |
89 | // testCreateBAODefaults fails without this. | |
90 | foreach ($apiRequest['params'] as $key => $param) { | |
91 | if ($key !== 'api.has_parent' && substr($key, 0, 4) === 'api.' || substr($key, 0, 4) === 'api_') { | |
92 | unset($apiRequest['params'][$key]); | |
93 | } | |
94 | } | |
95 | } | |
787604ff | 96 | $result = $function($apiRequest); |
d6f190fb | 97 | |
787604ff TO |
98 | } |
99 | elseif ($apiRequest['function'] && !$apiRequest['is_generic']) { | |
17617b96 | 100 | $result = $function($apiRequest['params']); |
787604ff TO |
101 | } |
102 | return $result; | |
103 | } | |
104 | ||
82376c19 | 105 | /** |
3dde2c6c | 106 | * @inheritDoc |
257e7666 EM |
107 | * @param int $version |
108 | * @return array | |
82376c19 | 109 | */ |
8882ff5c | 110 | public function getEntityNames($version) { |
82376c19 TO |
111 | $entities = array(); |
112 | $include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path())); | |
113 | #$include_dirs = array(dirname(__FILE__). '/../../'); | |
114 | foreach ($include_dirs as $include_dir) { | |
8882ff5c TO |
115 | $api_dir = implode(DIRECTORY_SEPARATOR, |
116 | array($include_dir, 'api', 'v' . $version)); | |
117 | if (!is_dir($api_dir)) { | |
82376c19 TO |
118 | continue; |
119 | } | |
120 | $iterator = new \DirectoryIterator($api_dir); | |
121 | foreach ($iterator as $fileinfo) { | |
122 | $file = $fileinfo->getFilename(); | |
123 | ||
124 | // Check for entities with a master file ("api/v3/MyEntity.php") | |
125 | $parts = explode(".", $file); | |
8882ff5c | 126 | if (end($parts) == "php" && $file != "utils.php" && !preg_match('/Tests?.php$/', $file)) { |
82376c19 TO |
127 | // without the ".php" |
128 | $entities[] = substr($file, 0, -4); | |
129 | } | |
130 | ||
8882ff5c | 131 | // Check for entities with standalone action files (eg "api/v3/MyEntity/MyAction.php"). |
82376c19 TO |
132 | $action_dir = $api_dir . DIRECTORY_SEPARATOR . $file; |
133 | if (preg_match('/^[A-Z][A-Za-z0-9]*$/', $file) && is_dir($action_dir)) { | |
134 | if (count(glob("$action_dir/[A-Z]*.php")) > 0) { | |
135 | $entities[] = $file; | |
136 | } | |
137 | } | |
138 | } | |
139 | } | |
140 | $entities = array_diff($entities, array('Generic')); | |
141 | $entities = array_unique($entities); | |
142 | sort($entities); | |
143 | ||
144 | return $entities; | |
145 | } | |
146 | ||
147 | /** | |
3dde2c6c | 148 | * @inheritDoc |
257e7666 EM |
149 | * @param int $version |
150 | * @param string $entity | |
151 | * @return array | |
82376c19 TO |
152 | */ |
153 | public function getActionNames($version, $entity) { | |
92776611 | 154 | $entity = _civicrm_api_get_camel_name($entity); |
82376c19 TO |
155 | $entities = $this->getEntityNames($version); |
156 | if (!in_array($entity, $entities)) { | |
157 | return array(); | |
c65db512 TO |
158 | } |
159 | $this->loadEntity($entity, $version); | |
160 | ||
161 | $functions = get_defined_functions(); | |
162 | $actions = array(); | |
6cbff284 | 163 | $prefix = 'civicrm_api' . $version . '_' . _civicrm_api_get_entity_name_from_camel($entity) . '_'; |
c65db512 TO |
164 | $prefixGeneric = 'civicrm_api' . $version . '_generic_'; |
165 | foreach ($functions['user'] as $fct) { | |
166 | if (strpos($fct, $prefix) === 0) { | |
167 | $actions[] = substr($fct, strlen($prefix)); | |
168 | } | |
169 | elseif (strpos($fct, $prefixGeneric) === 0) { | |
170 | $actions[] = substr($fct, strlen($prefixGeneric)); | |
171 | } | |
172 | } | |
173 | return $actions; | |
174 | } | |
175 | ||
176 | /** | |
fe482240 | 177 | * Look up the implementation for a given API request. |
c65db512 | 178 | * |
8882ff5c TO |
179 | * @param array $apiRequest |
180 | * Array with keys: | |
181 | * - entity: string, required. | |
182 | * - action: string, required. | |
183 | * - params: array. | |
184 | * - version: scalar, required. | |
c65db512 | 185 | * |
8882ff5c TO |
186 | * @return array |
187 | * Array with keys: | |
188 | * - function: callback (mixed) | |
189 | * - is_generic: boolean | |
c65db512 TO |
190 | */ |
191 | protected function resolve($apiRequest) { | |
192 | $cachekey = strtolower($apiRequest['entity']) . ':' . strtolower($apiRequest['action']) . ':' . $apiRequest['version']; | |
193 | if (isset($this->cache[$cachekey])) { | |
194 | return $this->cache[$cachekey]; | |
195 | } | |
196 | ||
197 | $camelName = _civicrm_api_get_camel_name($apiRequest['entity'], $apiRequest['version']); | |
198 | $actionCamelName = _civicrm_api_get_camel_name($apiRequest['action']); | |
199 | ||
200 | // Determine if there is an entity-specific implementation of the action | |
201 | $stdFunction = $this->getFunctionName($apiRequest['entity'], $apiRequest['action'], $apiRequest['version']); | |
202 | if (function_exists($stdFunction)) { | |
203 | // someone already loaded the appropriate file | |
8882ff5c TO |
204 | // FIXME: This has the affect of masking bugs in load order; this is |
205 | // included to provide bug-compatibility. | |
c65db512 TO |
206 | $this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE); |
207 | return $this->cache[$cachekey]; | |
208 | } | |
209 | ||
210 | $stdFiles = array( | |
8882ff5c TO |
211 | // By convention, the $camelName.php is more likely to contain the |
212 | // function, so test it first | |
c65db512 TO |
213 | 'api/v' . $apiRequest['version'] . '/' . $camelName . '.php', |
214 | 'api/v' . $apiRequest['version'] . '/' . $camelName . '/' . $actionCamelName . '.php', | |
215 | ); | |
216 | foreach ($stdFiles as $stdFile) { | |
217 | if (\CRM_Utils_File::isIncludable($stdFile)) { | |
218 | require_once $stdFile; | |
219 | if (function_exists($stdFunction)) { | |
220 | $this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE); | |
221 | return $this->cache[$cachekey]; | |
222 | } | |
223 | } | |
224 | } | |
225 | ||
226 | // Determine if there is a generic implementation of the action | |
227 | require_once 'api/v3/Generic.php'; | |
228 | # $genericFunction = 'civicrm_api3_generic_' . $apiRequest['action']; | |
229 | $genericFunction = $this->getFunctionName('generic', $apiRequest['action'], $apiRequest['version']); | |
230 | $genericFiles = array( | |
8882ff5c TO |
231 | // By convention, the Generic.php is more likely to contain the |
232 | // function, so test it first | |
c65db512 TO |
233 | 'api/v' . $apiRequest['version'] . '/Generic.php', |
234 | 'api/v' . $apiRequest['version'] . '/Generic/' . $actionCamelName . '.php', | |
235 | ); | |
236 | foreach ($genericFiles as $genericFile) { | |
237 | if (\CRM_Utils_File::isIncludable($genericFile)) { | |
238 | require_once $genericFile; | |
239 | if (function_exists($genericFunction)) { | |
240 | $this->cache[$cachekey] = array('function' => $genericFunction, 'is_generic' => TRUE); | |
241 | return $this->cache[$cachekey]; | |
242 | } | |
243 | } | |
244 | } | |
245 | ||
246 | $this->cache[$cachekey] = array('function' => FALSE, 'is_generic' => FALSE); | |
247 | return $this->cache[$cachekey]; | |
248 | } | |
249 | ||
250 | /** | |
8882ff5c TO |
251 | * Determine the function name for a given API request. |
252 | * | |
c65db512 | 253 | * @param string $entity |
8882ff5c | 254 | * API entity name. |
c65db512 | 255 | * @param string $action |
8882ff5c TO |
256 | * API action name. |
257 | * @param int $version | |
258 | * API version. | |
2a6da8d7 | 259 | * |
c65db512 TO |
260 | * @return string |
261 | */ | |
262 | protected function getFunctionName($entity, $action, $version) { | |
263 | $entity = _civicrm_api_get_entity_name_from_camel($entity); | |
264 | return 'civicrm_api' . $version . '_' . $entity . '_' . $action; | |
265 | } | |
266 | ||
267 | /** | |
268 | * Load/require all files related to an entity. | |
269 | * | |
270 | * This should not normally be called because it's does a file-system scan; it's | |
271 | * only appropriate when introspection is really required (eg for "getActions"). | |
272 | * | |
273 | * @param string $entity | |
8882ff5c | 274 | * API entity name. |
c65db512 | 275 | * @param int $version |
8882ff5c | 276 | * API version. |
c65db512 TO |
277 | */ |
278 | protected function loadEntity($entity, $version) { | |
279 | $camelName = _civicrm_api_get_camel_name($entity, $version); | |
280 | ||
281 | // Check for master entity file; to match _civicrm_api_resolve(), only load the first one | |
282 | $stdFile = 'api/v' . $version . '/' . $camelName . '.php'; | |
283 | if (\CRM_Utils_File::isIncludable($stdFile)) { | |
284 | require_once $stdFile; | |
285 | } | |
286 | ||
287 | // Check for standalone action files; to match _civicrm_api_resolve(), only load the first one | |
288 | $loaded_files = array(); // array($relativeFilePath => TRUE) | |
289 | $include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path())); | |
290 | foreach ($include_dirs as $include_dir) { | |
07007b7a | 291 | foreach (array($camelName, 'Generic') as $name) { |
8882ff5c TO |
292 | $action_dir = implode(DIRECTORY_SEPARATOR, |
293 | array($include_dir, 'api', "v${version}", $name)); | |
07007b7a CW |
294 | if (!is_dir($action_dir)) { |
295 | continue; | |
c65db512 TO |
296 | } |
297 | ||
07007b7a CW |
298 | $iterator = new \DirectoryIterator($action_dir); |
299 | foreach ($iterator as $fileinfo) { | |
300 | $file = $fileinfo->getFilename(); | |
301 | if (array_key_exists($file, $loaded_files)) { | |
302 | continue; // action provided by an earlier item on include_path | |
303 | } | |
304 | ||
305 | $parts = explode(".", $file); | |
306 | if (end($parts) == "php" && !preg_match('/Tests?\.php$/', $file)) { | |
307 | require_once $action_dir . DIRECTORY_SEPARATOR . $file; | |
308 | $loaded_files[$file] = TRUE; | |
309 | } | |
c65db512 TO |
310 | } |
311 | } | |
312 | } | |
313 | } | |
314 | ||
2a6da8d7 | 315 | } |