Merge pull request #16263 from eileenmcnaughton/ids_3
[civicrm-core.git] / CRM / Extension / Mapper.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 * This class proivdes various helper functions for locating extensions
14 * data. It's designed for compatibility with pre-existing functions from
15 * CRM_Core_Extensions.
16 *
17 * Most of these helper functions originate with the first major iteration
18 * of extensions -- a time when every extension had one eponymous PHP class,
19 * when there was no PHP class-loader, and when there was special-case logic
20 * sprinkled around to handle loading of "extension classes".
21 *
22 * With module-extensions (Civi 4.2+), there are no eponymous classes --
23 * instead, module-extensions follow the same class-naming and class-loading
24 * practices as core (and don't require special-case logic for class
25 * loading). Consequently, the helpers in here aren't much used with
26 * module-extensions.
27 *
28 * @package CRM
ca5cec67 29 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
30 */
31class CRM_Extension_Mapper {
32
33 /**
fe482240 34 * An URL for public extensions repository.
6a488035 35 */
6a488035
TO
36
37 /**
fe482240 38 * Extension info file name.
6a488035
TO
39 */
40 const EXT_TEMPLATES_DIRNAME = 'templates';
41
42 /**
43 * @var CRM_Extension_Container_Interface
44 */
45 protected $container;
46
47 /**
48 * @var array (key => CRM_Extension_Info)
49 */
be2fb01f 50 protected $infos = [];
6a488035
TO
51
52 /**
53 * @var array
54 */
55 protected $moduleExtensions = NULL;
56
57 /**
58 * @var CRM_Utils_Cache_Interface
59 */
60 protected $cache;
61
62 protected $cacheKey;
63
64 protected $civicrmPath;
65
66 protected $civicrmUrl;
67
e0ef6999
EM
68 /**
69 * @param CRM_Extension_Container_Interface $container
70 * @param CRM_Utils_Cache_Interface $cache
71 * @param null $cacheKey
72 * @param null $civicrmPath
73 * @param null $civicrmUrl
74 */
6a488035
TO
75 public function __construct(CRM_Extension_Container_Interface $container, CRM_Utils_Cache_Interface $cache = NULL, $cacheKey = NULL, $civicrmPath = NULL, $civicrmUrl = NULL) {
76 $this->container = $container;
77 $this->cache = $cache;
78 $this->cacheKey = $cacheKey;
79 if ($civicrmUrl) {
80 $this->civicrmUrl = rtrim($civicrmUrl, '/');
0db6c3e1
TO
81 }
82 else {
6a488035
TO
83 $config = CRM_Core_Config::singleton();
84 $this->civicrmUrl = rtrim($config->resourceBase, '/');
85 }
86 if ($civicrmPath) {
b3a4b879 87 $this->civicrmPath = rtrim($civicrmPath, '/');
0db6c3e1
TO
88 }
89 else {
6a488035 90 global $civicrm_root;
b3a4b879 91 $this->civicrmPath = rtrim($civicrm_root, '/');
6a488035
TO
92 }
93 }
94
95 /**
96 * Given the class, provides extension's key.
97 *
6a488035 98 *
f41911fd
TO
99 * @param string $clazz
100 * Extension class name.
6a488035 101 *
a6c01b45
CW
102 * @return string
103 * name of extension key
6a488035
TO
104 */
105 public function classToKey($clazz) {
106 return str_replace('_', '.', $clazz);
107 }
108
109 /**
110 * Given the class, provides extension path.
111 *
6a488035 112 *
6c8f6e67
EM
113 * @param $clazz
114 *
a6c01b45
CW
115 * @return string
116 * full path the extension .php file
6a488035
TO
117 */
118 public function classToPath($clazz) {
119 $elements = explode('_', $clazz);
120 $key = implode('.', $elements);
121 return $this->keyToPath($key);
122 }
123
124 /**
125 * Given the string, returns true or false if it's an extension key.
126 *
6a488035 127 *
f41911fd
TO
128 * @param string $key
129 * A string which might be an extension key.
6a488035 130 *
e7483cbe 131 * @return bool
a6c01b45 132 * true if given string is an extension name
6a488035
TO
133 */
134 public function isExtensionKey($key) {
135 // check if the string is an extension name or the class
136 return (strpos($key, '.') !== FALSE) ? TRUE : FALSE;
137 }
138
139 /**
140 * Given the string, returns true or false if it's an extension class name.
141 *
6a488035 142 *
f41911fd
TO
143 * @param string $clazz
144 * A string which might be an extension class name.
6a488035 145 *
e7483cbe 146 * @return bool
a6c01b45 147 * true if given string is an extension class name
6a488035
TO
148 */
149 public function isExtensionClass($clazz) {
150
151 if (substr($clazz, 0, 4) != 'CRM_') {
152 return (bool) preg_match('/^[a-z0-9]+(_[a-z0-9]+)+$/', $clazz);
153 }
154 return FALSE;
155 }
156
157 /**
f41911fd
TO
158 * @param string $key
159 * Extension fully-qualified-name.
77b97be7
EM
160 * @param bool $fresh
161 *
162 * @throws CRM_Extension_Exception
163 * @throws Exception
c490a46a 164 * @return CRM_Extension_Info
6a488035
TO
165 */
166 public function keyToInfo($key, $fresh = FALSE) {
167 if ($fresh || !array_key_exists($key, $this->infos)) {
168 try {
169 $this->infos[$key] = CRM_Extension_Info::loadFromFile($this->container->getPath($key) . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME);
0db6c3e1
TO
170 }
171 catch (CRM_Extension_Exception $e) {
6a488035
TO
172 // file has more detailed info, but we'll fallback to DB if it's missing -- DB has enough info to uninstall
173 $this->infos[$key] = CRM_Extension_System::singleton()->getManager()->createInfoFromDB($key);
174 if (!$this->infos[$key]) {
175 throw $e;
176 }
177 }
178 }
179 return $this->infos[$key];
180 }
181
182 /**
183 * Given the key, provides extension's class name.
184 *
6a488035 185 *
f41911fd
TO
186 * @param string $key
187 * Extension key.
6a488035 188 *
a6c01b45
CW
189 * @return string
190 * name of extension's main class
6a488035
TO
191 */
192 public function keyToClass($key) {
193 return str_replace('.', '_', $key);
194 }
195
196 /**
197 * Given the key, provides the path to file containing
198 * extension's main class.
199 *
6a488035 200 *
f41911fd
TO
201 * @param string $key
202 * Extension key.
6a488035 203 *
a6c01b45
CW
204 * @return string
205 * path to file containing extension's main class
6a488035
TO
206 */
207 public function keyToPath($key) {
208 $info = $this->keyToInfo($key);
209 return $this->container->getPath($key) . DIRECTORY_SEPARATOR . $info->file . '.php';
210 }
211
212 /**
213 * Given the key, provides the path to file containing
214 * extension's main class.
215 *
f41911fd
TO
216 * @param string $key
217 * Extension key.
a6c01b45
CW
218 * @return string
219 * local path of the extension source tree
6a488035
TO
220 */
221 public function keyToBasePath($key) {
222 if ($key == 'civicrm') {
223 return $this->civicrmPath;
224 }
225 return $this->container->getPath($key);
226 }
227
228 /**
229 * Given the key, provides the path to file containing
230 * extension's main class.
231 *
6a488035 232 *
f41911fd
TO
233 * @param string $key
234 * Extension key.
6a488035 235 *
a6c01b45
CW
236 * @return string
237 * url for resources in this extension
6a488035
TO
238 */
239 public function keyToUrl($key) {
240 if ($key == 'civicrm') {
dee7a2b1
PJ
241 // CRM-12130 Workaround: If the domain's config_backend is NULL at the start of the request,
242 // then the Mapper is wrongly constructed with an empty value for $this->civicrmUrl.
3d4a4ccf
PJ
243 if (empty($this->civicrmUrl)) {
244 $config = CRM_Core_Config::singleton();
245 return rtrim($config->resourceBase, '/');
246 }
6a488035
TO
247 return $this->civicrmUrl;
248 }
249
250 return $this->container->getResUrl($key);
251 }
252
253 /**
254 * Fetch the list of active extensions of type 'module'
255 *
5a4f6742
CW
256 * @param bool $fresh
257 * whether to forcibly reload extensions list from canonical store.
a6c01b45
CW
258 * @return array
259 * array(array('prefix' => $, 'file' => $))
6a488035
TO
260 */
261 public function getActiveModuleFiles($fresh = FALSE) {
62f662b0 262 if (!defined('CIVICRM_DSN')) {
263 // hmm, ok
264 return [];
6a488035
TO
265 }
266
267 $moduleExtensions = NULL;
268 if ($this->cache && !$fresh) {
ec2dd0bd 269 $moduleExtensions = $this->cache->get($this->cacheKey . '_moduleFiles');
6a488035
TO
270 }
271
272 if (!is_array($moduleExtensions)) {
6542d699
TO
273 $compat = CRM_Extension_System::getCompatibilityInfo();
274
6a488035 275 // Check canonical module list
be2fb01f 276 $moduleExtensions = [];
6a488035
TO
277 $sql = '
278 SELECT full_name, file
279 FROM civicrm_extension
280 WHERE is_active = 1
281 AND type = "module"
282 ';
283 $dao = CRM_Core_DAO::executeQuery($sql);
284 while ($dao->fetch()) {
6542d699
TO
285 if (!empty($compat[$dao->full_name]['force-uninstall'])) {
286 continue;
287 }
6a488035 288 try {
be2fb01f 289 $moduleExtensions[] = [
6a488035
TO
290 'prefix' => $dao->file,
291 'filePath' => $this->keyToPath($dao->full_name),
be2fb01f 292 ];
0db6c3e1
TO
293 }
294 catch (CRM_Extension_Exception $e) {
6a488035
TO
295 // Putting a stub here provides more consistency
296 // in how getActiveModuleFiles when racing between
297 // dirty file-removals and cache-clears.
298 CRM_Core_Session::setStatus($e->getMessage(), '', 'error');
be2fb01f 299 $moduleExtensions[] = [
6a488035
TO
300 'prefix' => $dao->file,
301 'filePath' => NULL,
be2fb01f 302 ];
6a488035
TO
303 }
304 }
305
306 if ($this->cache) {
ec2dd0bd 307 $this->cache->set($this->cacheKey . '_moduleFiles', $moduleExtensions);
6a488035
TO
308 }
309 }
310 return $moduleExtensions;
311 }
312
e7ff7042 313 /**
fe482240 314 * Get a list of base URLs for all active modules.
e7ff7042 315 *
a6c01b45
CW
316 * @return array
317 * (string $extKey => string $baseUrl)
e7ff7042
TO
318 */
319 public function getActiveModuleUrls() {
320 // TODO optimization/caching
be2fb01f 321 $urls = [];
e7ff7042
TO
322 $urls['civicrm'] = $this->keyToUrl('civicrm');
323 foreach ($this->getModules() as $module) {
324 /** @var $module CRM_Core_Module */
325 if ($module->is_active) {
326 $urls[$module->name] = $this->keyToUrl($module->name);
327 }
328 }
329 return $urls;
330 }
331
3b3f6d23
TO
332 /**
333 * Get a list of extension keys, filtered by the corresponding file path.
334 *
335 * @param string $pattern
336 * A file path. To search subdirectories, append "*".
337 * Ex: "/var/www/extensions/*"
338 * Ex: "/var/www/extensions/org.foo.bar"
339 * @return array
340 * Array(string $key).
341 * Ex: array("org.foo.bar").
342 */
343 public function getKeysByPath($pattern) {
be2fb01f 344 $keys = [];
3b3f6d23
TO
345
346 if (CRM_Utils_String::endsWith($pattern, '*')) {
347 $prefix = rtrim($pattern, '*');
348 foreach ($this->container->getKeys() as $key) {
349 $path = CRM_Utils_File::addTrailingSlash($this->container->getPath($key));
350 if (realpath($prefix) == realpath($path) || CRM_Utils_File::isChildPath($prefix, $path)) {
351 $keys[] = $key;
352 }
353 }
354 }
355 else {
356 foreach ($this->container->getKeys() as $key) {
357 $path = CRM_Utils_File::addTrailingSlash($this->container->getPath($key));
358 if (realpath($pattern) == realpath($path)) {
359 $keys[] = $key;
360 }
361 }
362 }
363
364 return $keys;
365 }
366
f8a7cfff
TO
367 /**
368 * @return array
369 * Ex: $result['org.civicrm.foobar'] = new CRM_Extension_Info(...).
370 * @throws \CRM_Extension_Exception
371 * @throws \Exception
372 */
373 public function getAllInfos() {
374 foreach ($this->container->getKeys() as $key) {
375 $this->keyToInfo($key);
376 }
377 return $this->infos;
378 }
379
e0ef6999 380 /**
100fef9d 381 * @param string $name
e0ef6999
EM
382 *
383 * @return bool
384 */
6a488035
TO
385 public function isActiveModule($name) {
386 $activeModules = $this->getActiveModuleFiles();
387 foreach ($activeModules as $activeModule) {
388 if ($activeModule['prefix'] == $name) {
389 return TRUE;
390 }
391 }
392 return FALSE;
393 }
394
395 /**
396 * Get a list of all installed modules, including enabled and disabled ones
397 *
a6c01b45
CW
398 * @return array
399 * CRM_Core_Module
6a488035
TO
400 */
401 public function getModules() {
be2fb01f 402 $result = [];
6a488035
TO
403 $dao = new CRM_Core_DAO_Extension();
404 $dao->type = 'module';
405 $dao->find();
406 while ($dao->fetch()) {
407 $result[] = new CRM_Core_Module($dao->full_name, $dao->is_active);
408 }
409 return $result;
410 }
411
412 /**
413 * Given the class, provides the template path.
414 *
6a488035 415 *
f41911fd
TO
416 * @param string $clazz
417 * Extension class name.
6a488035 418 *
a6c01b45
CW
419 * @return string
420 * path to extension's templates directory
6a488035
TO
421 */
422 public function getTemplatePath($clazz) {
423 $path = $this->container->getPath($this->classToKey($clazz));
424 return $path . DIRECTORY_SEPARATOR . self::EXT_TEMPLATES_DIRNAME;
425 /*
426 $path = $this->classToPath($clazz);
427 $pathElm = explode(DIRECTORY_SEPARATOR, $path);
428 array_pop($pathElm);
429 return implode(DIRECTORY_SEPARATOR, $pathElm) . DIRECTORY_SEPARATOR . self::EXT_TEMPLATES_DIRNAME;
e70a7fc0 430 */
6a488035
TO
431 }
432
433 /**
434 * Given te class, provides the template name.
435 * @todo consider multiple templates, support for one template for now
436 *
6a488035 437 *
f41911fd
TO
438 * @param string $clazz
439 * Extension class name.
6a488035 440 *
a6c01b45
CW
441 * @return string
442 * extension's template name
6a488035
TO
443 */
444 public function getTemplateName($clazz) {
445 $info = $this->keyToInfo($this->classToKey($clazz));
446 return (string) $info->file . '.tpl';
447 }
448
449 public function refresh() {
be2fb01f 450 $this->infos = [];
6a488035
TO
451 $this->moduleExtensions = NULL;
452 if ($this->cache) {
ec2dd0bd 453 $this->cache->delete($this->cacheKey . '_moduleFiles');
6a488035 454 }
85c7eb67
TO
455 // FIXME: How can code so code wrong be so right?
456 CRM_Extension_System::singleton()->getClassLoader()->refresh();
6a488035 457 }
96025800 458
6a488035 459}