3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 |
9 +--------------------------------------------------------------------+
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.
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".
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
29 * @copyright CiviCRM LLC https://civicrm.org/licensing
31 class CRM_Extension_Mapper
{
34 * An URL for public extensions repository.
38 * Extension info file name.
40 const EXT_TEMPLATES_DIRNAME
= 'templates';
43 * @var CRM_Extension_Container_Interface
48 * @var \CRM_Extension_Info[]
49 * (key => CRM_Extension_Info)
51 protected $infos = [];
56 protected $moduleExtensions = NULL;
59 * @var CRM_Utils_Cache_Interface
65 protected $civicrmPath;
67 protected $civicrmUrl;
70 * @param CRM_Extension_Container_Interface $container
71 * @param CRM_Utils_Cache_Interface $cache
72 * @param null $cacheKey
73 * @param null $civicrmPath
74 * @param null $civicrmUrl
76 public function __construct(CRM_Extension_Container_Interface
$container, CRM_Utils_Cache_Interface
$cache = NULL, $cacheKey = NULL, $civicrmPath = NULL, $civicrmUrl = NULL) {
77 $this->container
= $container;
78 $this->cache
= $cache;
79 $this->cacheKey
= $cacheKey;
81 $this->civicrmUrl
= rtrim($civicrmUrl, '/');
84 $config = CRM_Core_Config
::singleton();
85 $this->civicrmUrl
= rtrim($config->resourceBase
, '/');
88 $this->civicrmPath
= rtrim($civicrmPath, '/');
92 $this->civicrmPath
= rtrim($civicrm_root, '/');
97 * Given the class, provides extension's key.
100 * @param string $clazz
101 * Extension class name.
104 * name of extension key
106 public function classToKey($clazz) {
107 return str_replace('_', '.', $clazz);
111 * Given the class, provides extension path.
117 * full path the extension .php file
119 public function classToPath($clazz) {
120 $elements = explode('_', $clazz);
121 $key = implode('.', $elements);
122 return $this->keyToPath($key);
126 * Given the string, returns true or false if it's an extension key.
130 * A string which might be an extension key.
133 * true if given string is an extension name
135 public function isExtensionKey($key) {
136 // check if the string is an extension name or the class
137 return (strpos($key, '.') !== FALSE) ?
TRUE : FALSE;
141 * Given the string, returns true or false if it's an extension class name.
144 * @param string $clazz
145 * A string which might be an extension class name.
148 * true if given string is an extension class name
150 public function isExtensionClass($clazz) {
152 if (substr($clazz, 0, 4) != 'CRM_') {
153 return (bool) preg_match('/^[a-z0-9]+(_[a-z0-9]+)+$/', $clazz);
160 * Extension fully-qualified-name.
163 * @throws CRM_Extension_Exception
165 * @return CRM_Extension_Info
167 public function keyToInfo($key, $fresh = FALSE) {
168 if ($fresh ||
!array_key_exists($key, $this->infos
)) {
170 $this->infos
[$key] = CRM_Extension_Info
::loadFromFile($this->container
->getPath($key) . DIRECTORY_SEPARATOR
. CRM_Extension_Info
::FILENAME
);
172 catch (CRM_Extension_Exception
$e) {
173 // file has more detailed info, but we'll fallback to DB if it's missing -- DB has enough info to uninstall
174 $this->infos
[$key] = CRM_Extension_System
::singleton()->getManager()->createInfoFromDB($key);
175 if (!$this->infos
[$key]) {
180 return $this->infos
[$key];
184 * Given the key, provides extension's class name.
191 * name of extension's main class
193 public function keyToClass($key) {
194 return str_replace('.', '_', $key);
198 * Given the key, provides the path to file containing
199 * extension's main class.
206 * path to file containing extension's main class
208 public function keyToPath($key) {
209 $info = $this->keyToInfo($key);
210 return $this->container
->getPath($key) . DIRECTORY_SEPARATOR
. $info->file
. '.php';
214 * Given the key, provides the path to file containing
215 * extension's main class.
220 * local path of the extension source tree
222 public function keyToBasePath($key) {
223 if ($key == 'civicrm') {
224 return $this->civicrmPath
;
226 return $this->container
->getPath($key);
230 * Given the key, provides the path to file containing
231 * extension's main class.
238 * url for resources in this extension
240 public function keyToUrl($key) {
241 if ($key == 'civicrm') {
242 // CRM-12130 Workaround: If the domain's config_backend is NULL at the start of the request,
243 // then the Mapper is wrongly constructed with an empty value for $this->civicrmUrl.
244 if (empty($this->civicrmUrl
)) {
245 $config = CRM_Core_Config
::singleton();
246 return rtrim($config->resourceBase
, '/');
248 return $this->civicrmUrl
;
251 return $this->container
->getResUrl($key);
255 * Fetch the list of active extensions of type 'module'
258 * whether to forcibly reload extensions list from canonical store.
260 * array(array('prefix' => $, 'fullName' => $, 'filePath' => $))
262 public function getActiveModuleFiles($fresh = FALSE) {
263 if (!defined('CIVICRM_DSN')) {
268 // The list of module files is cached in two tiers. The tiers are slightly
271 // 1. The persistent tier (cache) stores
272 // names WITHOUT absolute paths.
273 // 2. The ephemeral/thread-local tier (statics) stores names
274 // WITH absolute paths.
275 // Return static value instead of re-running query
276 if (isset(Civi
::$statics[__CLASS__
]['moduleExtensions']) && !$fresh) {
277 return Civi
::$statics[__CLASS__
]['moduleExtensions'];
280 $moduleExtensions = NULL;
282 // Checked if it's stored in the persistent cache.
283 if ($this->cache
&& !$fresh) {
284 $moduleExtensions = $this->cache
->get($this->cacheKey
. '_moduleFiles');
287 // If cache is empty we build it from database.
288 if (!is_array($moduleExtensions)) {
289 $compat = CRM_Extension_System
::getCompatibilityInfo();
291 // Check canonical module list
292 $moduleExtensions = [];
294 SELECT full_name, file
295 FROM civicrm_extension
299 $dao = CRM_Core_DAO
::executeQuery($sql);
300 while ($dao->fetch()) {
301 if (!empty($compat[$dao->full_name
]['force-uninstall'])) {
304 $moduleExtensions[] = [
305 'prefix' => $dao->file
,
306 'fullName' => $dao->full_name
,
312 $this->cache
->set($this->cacheKey
. '_moduleFiles', $moduleExtensions);
316 // Since we're not caching the full path we add it now.
317 array_walk($moduleExtensions, function(&$value, $key) {
319 if (!$value['filePath']) {
320 $value['filePath'] = $this->keyToPath($value['fullName']);
323 catch (CRM_Extension_Exception
$e) {
324 // Putting a stub here provides more consistency
325 // in how getActiveModuleFiles when racing between
326 // dirty file-removals and cache-clears.
327 CRM_Core_Session
::setStatus($e->getMessage(), '', 'error');
328 $value['filePath'] = NULL;
332 Civi
::$statics[__CLASS__
]['moduleExtensions'] = $moduleExtensions;
334 return $moduleExtensions;
338 * Get a list of base URLs for all active modules.
341 * (string $extKey => string $baseUrl)
343 public function getActiveModuleUrls() {
344 // TODO optimization/caching
346 $urls['civicrm'] = $this->keyToUrl('civicrm');
347 foreach ($this->getModules() as $module) {
348 /** @var $module CRM_Core_Module */
349 if ($module->is_active
) {
350 $urls[$module->name
] = $this->keyToUrl($module->name
);
357 * Get a list of extension keys, filtered by the corresponding file path.
359 * @param string $pattern
360 * A file path. To search subdirectories, append "*".
361 * Ex: "/var/www/extensions/*"
362 * Ex: "/var/www/extensions/org.foo.bar"
364 * Array(string $key).
365 * Ex: array("org.foo.bar").
367 public function getKeysByPath($pattern) {
370 if (CRM_Utils_String
::endsWith($pattern, '*')) {
371 $prefix = rtrim($pattern, '*');
372 foreach ($this->container
->getKeys() as $key) {
373 $path = CRM_Utils_File
::addTrailingSlash($this->container
->getPath($key));
374 if (realpath($prefix) == realpath($path) || CRM_Utils_File
::isChildPath($prefix, $path)) {
380 foreach ($this->container
->getKeys() as $key) {
381 $path = CRM_Utils_File
::addTrailingSlash($this->container
->getPath($key));
382 if (realpath($pattern) == realpath($path)) {
393 * Ex: $result['org.civicrm.foobar'] = new CRM_Extension_Info(...).
394 * @throws \CRM_Extension_Exception
397 public function getAllInfos() {
398 foreach ($this->container
->getKeys() as $key) {
399 $this->keyToInfo($key);
405 * @param string $name
409 public function isActiveModule($name) {
410 $activeModules = $this->getActiveModuleFiles();
411 foreach ($activeModules as $activeModule) {
412 if ($activeModule['prefix'] == $name) {
420 * Get a list of all installed modules, including enabled and disabled ones
425 public function getModules() {
427 $dao = new CRM_Core_DAO_Extension();
428 $dao->type
= 'module';
430 while ($dao->fetch()) {
431 $result[] = new CRM_Core_Module($dao->full_name
, $dao->is_active
);
437 * Given the class, provides the template path.
440 * @param string $clazz
441 * Extension class name.
444 * path to extension's templates directory
446 public function getTemplatePath($clazz) {
447 $path = $this->container
->getPath($this->classToKey($clazz));
448 return $path . DIRECTORY_SEPARATOR
. self
::EXT_TEMPLATES_DIRNAME
;
450 $path = $this->classToPath($clazz);
451 $pathElm = explode(DIRECTORY_SEPARATOR, $path);
453 return implode(DIRECTORY_SEPARATOR, $pathElm) . DIRECTORY_SEPARATOR . self::EXT_TEMPLATES_DIRNAME;
458 * Given te class, provides the template name.
459 * @todo consider multiple templates, support for one template for now
462 * @param string $clazz
463 * Extension class name.
466 * extension's template name
468 public function getTemplateName($clazz) {
469 $info = $this->keyToInfo($this->classToKey($clazz));
470 return (string) $info->file
. '.tpl';
473 public function refresh() {
475 $this->moduleExtensions
= NULL;
477 $this->cache
->delete($this->cacheKey
. '_moduleFiles');
479 // FIXME: How can code so code wrong be so right?
480 CRM_Extension_System
::singleton()->getClassLoader()->refresh();