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;
71 * Array(string $extKey => CRM_Extension_Upgrader_Interface $upgrader)
73 protected $upgraders = [];
76 * @param CRM_Extension_Container_Interface $container
77 * @param CRM_Utils_Cache_Interface $cache
78 * @param null $cacheKey
79 * @param null $civicrmPath
80 * @param null $civicrmUrl
82 public function __construct(CRM_Extension_Container_Interface
$container, CRM_Utils_Cache_Interface
$cache = NULL, $cacheKey = NULL, $civicrmPath = NULL, $civicrmUrl = NULL) {
83 $this->container
= $container;
84 $this->cache
= $cache;
85 $this->cacheKey
= $cacheKey;
87 $this->civicrmUrl
= rtrim($civicrmUrl, '/');
90 $config = CRM_Core_Config
::singleton();
91 $this->civicrmUrl
= rtrim($config->resourceBase
, '/');
94 $this->civicrmPath
= rtrim($civicrmPath, '/');
98 $this->civicrmPath
= rtrim($civicrm_root, '/');
103 * Given the class, provides extension's key.
106 * @param string $clazz
107 * Extension class name.
110 * name of extension key
112 public function classToKey($clazz) {
113 return str_replace('_', '.', $clazz);
117 * Given the class, provides extension path.
123 * full path the extension .php file
125 public function classToPath($clazz) {
126 $elements = explode('_', $clazz);
127 $key = implode('.', $elements);
128 return $this->keyToPath($key);
132 * Given the string, returns true or false if it's an extension key.
136 * A string which might be an extension key.
139 * true if given string is an extension name
141 public function isExtensionKey($key) {
142 // check if the string is an extension name or the class
143 return (strpos($key, '.') !== FALSE) ?
TRUE : FALSE;
147 * Given the string, returns true or false if it's an extension class name.
150 * @param string $clazz
151 * A string which might be an extension class name.
154 * true if given string is an extension class name
156 public function isExtensionClass($clazz) {
158 if (substr($clazz, 0, 4) != 'CRM_') {
159 return (bool) preg_match('/^[a-z0-9]+(_[a-z0-9]+)+$/', $clazz);
166 * Extension fully-qualified-name.
169 * @throws CRM_Extension_Exception
171 * @return CRM_Extension_Info
173 public function keyToInfo($key, $fresh = FALSE) {
174 if ($fresh ||
!array_key_exists($key, $this->infos
)) {
176 $this->infos
[$key] = CRM_Extension_Info
::loadFromFile($this->container
->getPath($key) . DIRECTORY_SEPARATOR
. CRM_Extension_Info
::FILENAME
);
178 catch (CRM_Extension_Exception
$e) {
179 // file has more detailed info, but we'll fallback to DB if it's missing -- DB has enough info to uninstall
180 $dbInfo = CRM_Extension_System
::singleton()->getManager()->createInfoFromDB($key);
184 $this->infos
[$key] = $dbInfo;
187 return $this->infos
[$key];
191 * Given the key, provides extension's class name.
198 * name of extension's main class
200 public function keyToClass($key) {
201 return str_replace('.', '_', $key);
205 * Given the key, provides the path to file containing
206 * extension's main class.
213 * path to file containing extension's main class
215 public function keyToPath($key) {
216 $info = $this->keyToInfo($key);
217 return $this->container
->getPath($key) . DIRECTORY_SEPARATOR
. $info->file
. '.php';
221 * Given the key, provides the path to file containing
222 * extension's main class.
227 * local path of the extension source tree
229 public function keyToBasePath($key) {
230 if ($key == 'civicrm') {
231 return $this->civicrmPath
;
233 return $this->container
->getPath($key);
237 * Given the key, provides the path to file containing
238 * extension's main class.
245 * url for resources in this extension
247 * @throws \CRM_Extension_Exception_MissingException
249 public function keyToUrl($key) {
250 if ($key === 'civicrm') {
251 // CRM-12130 Workaround: If the domain's config_backend is NULL at the start of the request,
252 // then the Mapper is wrongly constructed with an empty value for $this->civicrmUrl.
253 if (empty($this->civicrmUrl
)) {
254 $config = CRM_Core_Config
::singleton();
255 return rtrim($config->resourceBase
, '/');
257 return $this->civicrmUrl
;
260 return $this->container
->getResUrl($key);
264 * Fetch the list of active extensions of type 'module'
267 * whether to forcibly reload extensions list from canonical store.
269 * array(array('prefix' => $, 'fullName' => $, 'filePath' => $))
271 public function getActiveModuleFiles($fresh = FALSE) {
272 if (!defined('CIVICRM_DSN')) {
277 // The list of module files is cached in two tiers. The tiers are slightly
280 // 1. The persistent tier (cache) stores
281 // names WITHOUT absolute paths.
282 // 2. The ephemeral/thread-local tier (statics) stores names
283 // WITH absolute paths.
284 // Return static value instead of re-running query
285 if (isset(Civi
::$statics[__CLASS__
]['moduleExtensions']) && !$fresh) {
286 return Civi
::$statics[__CLASS__
]['moduleExtensions'];
289 $moduleExtensions = NULL;
291 // Checked if it's stored in the persistent cache.
292 if ($this->cache
&& !$fresh) {
293 $moduleExtensions = $this->cache
->get($this->cacheKey
. '_moduleFiles');
296 // If cache is empty we build it from database.
297 if (!is_array($moduleExtensions)) {
298 $compat = CRM_Extension_System
::getCompatibilityInfo();
300 // Check canonical module list
301 $moduleExtensions = [];
303 SELECT full_name, file
304 FROM civicrm_extension
308 $dao = CRM_Core_DAO
::executeQuery($sql);
309 while ($dao->fetch()) {
310 if (!empty($compat[$dao->full_name
]['force-uninstall'])) {
313 $moduleExtensions[] = [
314 'prefix' => $dao->file
,
315 'fullName' => $dao->full_name
,
321 $this->cache
->set($this->cacheKey
. '_moduleFiles', $moduleExtensions);
325 // Since we're not caching the full path we add it now.
326 array_walk($moduleExtensions, function(&$value, $key) {
328 if (!$value['filePath']) {
329 $value['filePath'] = $this->keyToPath($value['fullName']);
332 catch (CRM_Extension_Exception
$e) {
333 // Putting a stub here provides more consistency
334 // in how getActiveModuleFiles when racing between
335 // dirty file-removals and cache-clears.
336 CRM_Core_Session
::setStatus($e->getMessage(), '', 'error');
337 $value['filePath'] = NULL;
341 Civi
::$statics[__CLASS__
]['moduleExtensions'] = $moduleExtensions;
343 return $moduleExtensions;
347 * Get a list of base URLs for all active modules.
350 * (string $extKey => string $baseUrl)
352 * @throws \CRM_Extension_Exception_MissingException
354 public function getActiveModuleUrls() {
355 // TODO optimization/caching
357 $urls['civicrm'] = $this->keyToUrl('civicrm');
358 foreach ($this->getModules() as $module) {
359 /** @var $module CRM_Core_Module */
360 if ($module->is_active
) {
362 $urls[$module->name
] = $this->keyToUrl($module->name
);
364 catch (CRM_Extension_Exception_MissingException
$e) {
365 CRM_Core_Session
::setStatus(ts('An enabled extension is missing from the extensions directory') . ':' . $module->name
);
373 * Get a list of extension keys, filtered by the corresponding file path.
375 * @param string $pattern
376 * A file path. To search subdirectories, append "*".
377 * Ex: "/var/www/extensions/*"
378 * Ex: "/var/www/extensions/org.foo.bar"
380 * Array(string $key).
381 * Ex: array("org.foo.bar").
383 public function getKeysByPath($pattern) {
386 if (CRM_Utils_String
::endsWith($pattern, '*')) {
387 $prefix = rtrim($pattern, '*');
388 foreach ($this->container
->getKeys() as $key) {
389 $path = CRM_Utils_File
::addTrailingSlash($this->container
->getPath($key));
390 if (realpath($prefix) == realpath($path) || CRM_Utils_File
::isChildPath($prefix, $path)) {
396 foreach ($this->container
->getKeys() as $key) {
397 $path = CRM_Utils_File
::addTrailingSlash($this->container
->getPath($key));
398 if (realpath($pattern) == realpath($path)) {
408 * Get a list of extensions which match a given tag.
413 * Array(string $key).
414 * Ex: array("org.foo.bar").
416 public function getKeysByTag($tag) {
417 $allTags = $this->getAllTags();
418 return $allTags[$tag] ??
[];
422 * Get a list of extension tags.
425 * Ex: ['form-building' => ['org.civicrm.afform-gui', 'org.civicrm.afform-html']]
427 public function getAllTags() {
428 $tags = Civi
::cache('short')->get('extension_tags', NULL);
429 if ($tags !== NULL) {
434 $allInfos = $this->getAllInfos();
435 foreach ($allInfos as $key => $info) {
436 foreach ($info->tags
as $tag) {
437 $tags[$tag][] = $key;
444 * @return CRM_Extension_Info[]
445 * Ex: $result['org.civicrm.foobar'] = new CRM_Extension_Info(...).
446 * @throws \CRM_Extension_Exception
449 public function getAllInfos() {
450 foreach ($this->container
->getKeys() as $key) {
452 $this->keyToInfo($key);
454 catch (CRM_Extension_Exception_ParseException
$e) {
455 CRM_Core_Session
::setStatus(ts('Parse error in extension: %1', [
456 1 => $e->getMessage(),
458 CRM_Core_Error
::debug_log_message("Parse error in extension: " . $e->getMessage());
466 * @param string $name
470 public function isActiveModule($name) {
471 $activeModules = $this->getActiveModuleFiles();
472 foreach ($activeModules as $activeModule) {
473 if ($activeModule['prefix'] == $name) {
481 * Get a list of all installed modules, including enabled and disabled ones
483 * @return CRM_Core_Module[]
485 public function getModules() {
487 $dao = new CRM_Core_DAO_Extension();
488 $dao->type
= 'module';
490 while ($dao->fetch()) {
491 $result[] = new CRM_Core_Module($dao->full_name
, $dao->is_active
);
497 * Given the class, provides the template path.
500 * @param string $clazz
501 * Extension class name.
504 * path to extension's templates directory
506 public function getTemplatePath($clazz) {
507 $path = $this->container
->getPath($this->classToKey($clazz));
508 return $path . DIRECTORY_SEPARATOR
. self
::EXT_TEMPLATES_DIRNAME
;
510 $path = $this->classToPath($clazz);
511 $pathElm = explode(DIRECTORY_SEPARATOR, $path);
513 return implode(DIRECTORY_SEPARATOR, $pathElm) . DIRECTORY_SEPARATOR . self::EXT_TEMPLATES_DIRNAME;
518 * Given te class, provides the template name.
519 * @todo consider multiple templates, support for one template for now
522 * @param string $clazz
523 * Extension class name.
526 * extension's template name
528 public function getTemplateName($clazz) {
529 $info = $this->keyToInfo($this->classToKey($clazz));
530 return (string) $info->file
. '.tpl';
533 public function refresh() {
535 $this->moduleExtensions
= NULL;
537 $this->cache
->delete($this->cacheKey
. '_moduleFiles');
539 // FIXME: How can code so code wrong be so right?
540 CRM_Extension_System
::singleton()->getClassLoader()->refresh();
541 CRM_Extension_System
::singleton()->getMixinLoader()->run(TRUE);
545 * This returns a formatted string containing an extension upgrade link for the UI.
546 * @todo We should improve this to return more appropriate text. eg. when an extension is not installed
547 * it should not say "version xx is installed".
549 * @param CRM_Extension_Info $remoteExtensionInfo
550 * @param array $localExtensionInfo
554 public function getUpgradeLink($remoteExtensionInfo, $localExtensionInfo) {
555 if (!empty($remoteExtensionInfo) && version_compare($localExtensionInfo['version'], $remoteExtensionInfo->version
, '<')) {
556 return ts('Version %1 is installed. <a %2>Upgrade to version %3</a>.', [
557 1 => $localExtensionInfo['version'],
558 2 => 'href="' . CRM_Utils_System
::url('civicrm/admin/extensions', "action=update&id={$localExtensionInfo['key']}&key={$localExtensionInfo['key']}") . '"',
559 3 => $remoteExtensionInfo->version
,
566 * Long name of the extension.
567 * Ex: 'org.example.myext'
569 * @return \CRM_Extension_Upgrader_Interface
571 public function getUpgrader(string $key) {
572 if (!array_key_exists($key, $this->upgraders
)) {
573 $this->upgraders
[$key] = NULL;
576 $info = $this->keyToInfo($key);
578 catch (CRM_Extension_Exception_ParseException
$e) {
579 CRM_Core_Session
::setStatus(ts('Parse error in extension: %1', [
580 1 => $e->getMessage(),
582 CRM_Core_Error
::debug_log_message("Parse error in extension: " . $e->getMessage());
586 if (!empty($info->upgrader
)) {
587 $class = $info->upgrader
;
589 $u->init(['key' => $key]);
590 $this->upgraders
[$key] = $u;
593 return $this->upgraders
[$key];