| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | Copyright CiviCRM LLC. All rights reserved. | |
| 5 | | | |
| 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 | +--------------------------------------------------------------------+ |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | * This class glues together the various parts of the extension |
| 14 | * system. |
| 15 | * |
| 16 | * @package CRM |
| 17 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
| 18 | */ |
| 19 | class CRM_Extension_System { |
| 20 | private static $singleton; |
| 21 | |
| 22 | private $cache = NULL; |
| 23 | private $fullContainer = NULL; |
| 24 | private $defaultContainer = NULL; |
| 25 | private $mapper = NULL; |
| 26 | private $manager = NULL; |
| 27 | private $browser = NULL; |
| 28 | private $downloader = NULL; |
| 29 | private $mixinLoader = NULL; |
| 30 | |
| 31 | /** |
| 32 | * @var CRM_Extension_ClassLoader |
| 33 | * */ |
| 34 | private $classLoader; |
| 35 | |
| 36 | /** |
| 37 | * The URL of the remote extensions repository. |
| 38 | * |
| 39 | * @var string|false |
| 40 | */ |
| 41 | private $_repoUrl = NULL; |
| 42 | |
| 43 | /** |
| 44 | * @var array |
| 45 | * Construction parameters. These are primarily retained so |
| 46 | * that they can influence the cache name. |
| 47 | */ |
| 48 | protected $parameters; |
| 49 | |
| 50 | /** |
| 51 | * @param bool $fresh |
| 52 | * TRUE to force creation of a new system. |
| 53 | * |
| 54 | * @return CRM_Extension_System |
| 55 | */ |
| 56 | public static function singleton($fresh = FALSE) { |
| 57 | if (!self::$singleton || $fresh) { |
| 58 | if (self::$singleton) { |
| 59 | self::$singleton = new CRM_Extension_System(self::$singleton->parameters); |
| 60 | } |
| 61 | else { |
| 62 | self::$singleton = new CRM_Extension_System(); |
| 63 | } |
| 64 | } |
| 65 | return self::$singleton; |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * @param CRM_Extension_System $singleton |
| 70 | * The new, singleton extension system. |
| 71 | */ |
| 72 | public static function setSingleton(CRM_Extension_System $singleton) { |
| 73 | self::$singleton = $singleton; |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * Class constructor. |
| 78 | * |
| 79 | * @param array $parameters |
| 80 | * List of configuration values required by the extension system. |
| 81 | * Missing values will be guessed based on $config. |
| 82 | */ |
| 83 | public function __construct($parameters = []) { |
| 84 | $config = CRM_Core_Config::singleton(); |
| 85 | $parameters['extensionsDir'] = CRM_Utils_Array::value('extensionsDir', $parameters, $config->extensionsDir); |
| 86 | $parameters['extensionsURL'] = CRM_Utils_Array::value('extensionsURL', $parameters, $config->extensionsURL); |
| 87 | $parameters['resourceBase'] = CRM_Utils_Array::value('resourceBase', $parameters, $config->resourceBase); |
| 88 | $parameters['uploadDir'] = CRM_Utils_Array::value('uploadDir', $parameters, $config->uploadDir); |
| 89 | $parameters['userFrameworkBaseURL'] = CRM_Utils_Array::value('userFrameworkBaseURL', $parameters, $config->userFrameworkBaseURL); |
| 90 | if (!array_key_exists('civicrm_root', $parameters)) { |
| 91 | $parameters['civicrm_root'] = $GLOBALS['civicrm_root']; |
| 92 | } |
| 93 | if (!array_key_exists('cmsRootPath', $parameters)) { |
| 94 | $parameters['cmsRootPath'] = $config->userSystem->cmsRootPath(); |
| 95 | } |
| 96 | if (!array_key_exists('domain_id', $parameters)) { |
| 97 | $parameters['domain_id'] = CRM_Core_Config::domainID(); |
| 98 | } |
| 99 | // guaranteed ordering - useful for md5(serialize($parameters)) |
| 100 | ksort($parameters); |
| 101 | |
| 102 | $this->parameters = $parameters; |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Get a container which represents all available extensions. |
| 107 | * |
| 108 | * @return CRM_Extension_Container_Interface |
| 109 | */ |
| 110 | public function getFullContainer() { |
| 111 | if ($this->fullContainer === NULL) { |
| 112 | $containers = []; |
| 113 | |
| 114 | if ($this->getDefaultContainer()) { |
| 115 | $containers['default'] = $this->getDefaultContainer(); |
| 116 | } |
| 117 | |
| 118 | $containers['civiroot'] = new CRM_Extension_Container_Basic( |
| 119 | $this->parameters['civicrm_root'], |
| 120 | $this->parameters['resourceBase'], |
| 121 | $this->getCache(), |
| 122 | 'civiroot' |
| 123 | ); |
| 124 | |
| 125 | // TODO: CRM_Extension_Container_Basic( /sites/all/modules ) |
| 126 | // TODO: CRM_Extension_Container_Basic( /sites/$domain/modules |
| 127 | // TODO: CRM_Extension_Container_Basic( /modules ) |
| 128 | // TODO: CRM_Extension_Container_Basic( /vendors ) |
| 129 | |
| 130 | // At time of writing, D6, D7, and WP support cmsRootPath() but J does not |
| 131 | if (NULL !== $this->parameters['cmsRootPath']) { |
| 132 | $vendorPath = $this->parameters['cmsRootPath'] . DIRECTORY_SEPARATOR . 'vendor'; |
| 133 | if (is_dir($vendorPath)) { |
| 134 | $containers['cmsvendor'] = new CRM_Extension_Container_Basic( |
| 135 | $vendorPath, |
| 136 | CRM_Utils_File::addTrailingSlash($this->parameters['userFrameworkBaseURL'], '/') . 'vendor', |
| 137 | $this->getCache(), |
| 138 | 'cmsvendor' |
| 139 | ); |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | if (!defined('CIVICRM_TEST')) { |
| 144 | foreach ($containers as $container) { |
| 145 | $container->addFilter([__CLASS__, 'isNotTestExtension']); |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | $this->fullContainer = new CRM_Extension_Container_Collection($containers, $this->getCache(), 'full'); |
| 150 | } |
| 151 | return $this->fullContainer; |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Get the container to which new extensions are installed. |
| 156 | * |
| 157 | * This container should be a particular, writeable directory. |
| 158 | * |
| 159 | * @return CRM_Extension_Container_Default|FALSE (false if not configured) |
| 160 | */ |
| 161 | public function getDefaultContainer() { |
| 162 | if ($this->defaultContainer === NULL) { |
| 163 | if ($this->parameters['extensionsDir']) { |
| 164 | $this->defaultContainer = new CRM_Extension_Container_Default($this->parameters['extensionsDir'], $this->parameters['extensionsURL'], $this->getCache(), 'default'); |
| 165 | } |
| 166 | else { |
| 167 | $this->defaultContainer = FALSE; |
| 168 | } |
| 169 | } |
| 170 | return $this->defaultContainer; |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Get the service which provides runtime information about extensions. |
| 175 | * |
| 176 | * @return CRM_Extension_Mapper |
| 177 | */ |
| 178 | public function getMapper() { |
| 179 | if ($this->mapper === NULL) { |
| 180 | $this->mapper = new CRM_Extension_Mapper($this->getFullContainer(), $this->getCache(), 'mapper'); |
| 181 | } |
| 182 | return $this->mapper; |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * @return \CRM_Extension_ClassLoader |
| 187 | */ |
| 188 | public function getClassLoader() { |
| 189 | if ($this->classLoader === NULL) { |
| 190 | $this->classLoader = new CRM_Extension_ClassLoader($this->getMapper(), $this->getFullContainer(), $this->getManager()); |
| 191 | } |
| 192 | return $this->classLoader; |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Get the service for enabling and disabling extensions. |
| 197 | * |
| 198 | * @return CRM_Extension_Manager |
| 199 | */ |
| 200 | public function getManager() { |
| 201 | if ($this->manager === NULL) { |
| 202 | $typeManagers = [ |
| 203 | 'payment' => new CRM_Extension_Manager_Payment($this->getMapper()), |
| 204 | 'report' => new CRM_Extension_Manager_Report(), |
| 205 | 'search' => new CRM_Extension_Manager_Search(), |
| 206 | 'module' => new CRM_Extension_Manager_Module($this->getMapper()), |
| 207 | ]; |
| 208 | $this->manager = new CRM_Extension_Manager($this->getFullContainer(), $this->getDefaultContainer(), $this->getMapper(), $typeManagers); |
| 209 | } |
| 210 | return $this->manager; |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * Get the service for finding remotely-available extensions |
| 215 | * |
| 216 | * @return CRM_Extension_Browser |
| 217 | */ |
| 218 | public function getBrowser() { |
| 219 | if ($this->browser === NULL) { |
| 220 | $cacheDir = NULL; |
| 221 | if (!empty($this->parameters['uploadDir'])) { |
| 222 | $cacheDir = CRM_Utils_File::addTrailingSlash($this->parameters['uploadDir']) . 'cache'; |
| 223 | } |
| 224 | $this->browser = new CRM_Extension_Browser($this->getRepositoryUrl(), '', $cacheDir); |
| 225 | } |
| 226 | return $this->browser; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Get the service for loading code from remotely-available extensions |
| 231 | * |
| 232 | * @return CRM_Extension_Downloader |
| 233 | */ |
| 234 | public function getDownloader() { |
| 235 | if ($this->downloader === NULL) { |
| 236 | $basedir = ($this->getDefaultContainer() ? $this->getDefaultContainer()->getBaseDir() : NULL); |
| 237 | $this->downloader = new CRM_Extension_Downloader( |
| 238 | $this->getManager(), |
| 239 | $basedir, |
| 240 | // WAS: $config->extensionsDir . DIRECTORY_SEPARATOR . 'tmp'; |
| 241 | CRM_Utils_File::tempdir() |
| 242 | ); |
| 243 | } |
| 244 | return $this->downloader; |
| 245 | } |
| 246 | |
| 247 | /** |
| 248 | * @return CRM_Extension_MixinLoader; |
| 249 | */ |
| 250 | public function getMixinLoader() { |
| 251 | if ($this->mixinLoader === NULL) { |
| 252 | $this->mixinLoader = new CRM_Extension_MixinLoader(); |
| 253 | } |
| 254 | return $this->mixinLoader; |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * @return CRM_Utils_Cache_Interface |
| 259 | */ |
| 260 | public function getCache() { |
| 261 | if ($this->cache === NULL) { |
| 262 | $cacheGroup = md5(serialize(['ext', $this->parameters, CRM_Utils_System::version()])); |
| 263 | // Extension system starts before container. Manage our own cache. |
| 264 | $this->cache = CRM_Utils_Cache::create([ |
| 265 | 'name' => $cacheGroup, |
| 266 | 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'], |
| 267 | 'prefetch' => TRUE, |
| 268 | ]); |
| 269 | } |
| 270 | return $this->cache; |
| 271 | } |
| 272 | |
| 273 | /** |
| 274 | * Determine the URL which provides a feed of available extensions. |
| 275 | * |
| 276 | * @return string|FALSE |
| 277 | */ |
| 278 | public function getRepositoryUrl() { |
| 279 | if (empty($this->_repoUrl) && $this->_repoUrl !== FALSE) { |
| 280 | $url = Civi::settings()->get('ext_repo_url'); |
| 281 | |
| 282 | // boolean false means don't try to check extensions |
| 283 | // CRM-10575 |
| 284 | if ($url === FALSE) { |
| 285 | $this->_repoUrl = FALSE; |
| 286 | } |
| 287 | else { |
| 288 | $this->_repoUrl = CRM_Utils_System::evalUrl($url); |
| 289 | } |
| 290 | } |
| 291 | return $this->_repoUrl; |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Returns a list keyed by extension key |
| 296 | * |
| 297 | * @return array |
| 298 | */ |
| 299 | public static function getCompatibilityInfo() { |
| 300 | if (!isset(Civi::$statics[__CLASS__]['compatibility'])) { |
| 301 | Civi::$statics[__CLASS__]['compatibility'] = json_decode(file_get_contents(Civi::paths()->getPath('[civicrm.root]/extension-compatibility.json')), TRUE); |
| 302 | } |
| 303 | return Civi::$statics[__CLASS__]['compatibility']; |
| 304 | } |
| 305 | |
| 306 | public static function isNotTestExtension(CRM_Extension_Info $info) { |
| 307 | return (bool) !preg_match('/^test\./', $info->key); |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Take an extension's raw XML info and add information about the |
| 312 | * extension's status on the local system. |
| 313 | * |
| 314 | * The result format resembles the old CRM_Core_Extensions_Extension. |
| 315 | * |
| 316 | * @param CRM_Extension_Info $obj |
| 317 | * |
| 318 | * @return array |
| 319 | */ |
| 320 | public static function createExtendedInfo(CRM_Extension_Info $obj) { |
| 321 | $mapper = CRM_Extension_System::singleton()->getMapper(); |
| 322 | $manager = CRM_Extension_System::singleton()->getManager(); |
| 323 | |
| 324 | $extensionRow = (array) $obj; |
| 325 | try { |
| 326 | $extensionRow['path'] = $mapper->keyToBasePath($obj->key); |
| 327 | } |
| 328 | catch (CRM_Extension_Exception $e) { |
| 329 | $extensionRow['path'] = ''; |
| 330 | } |
| 331 | $extensionRow['status'] = $manager->getStatus($obj->key); |
| 332 | |
| 333 | switch ($extensionRow['status']) { |
| 334 | case CRM_Extension_Manager::STATUS_UNINSTALLED: |
| 335 | // ts('Uninstalled'); |
| 336 | $extensionRow['statusLabel'] = ''; |
| 337 | break; |
| 338 | |
| 339 | case CRM_Extension_Manager::STATUS_DISABLED: |
| 340 | $extensionRow['statusLabel'] = ts('Disabled'); |
| 341 | break; |
| 342 | |
| 343 | case CRM_Extension_Manager::STATUS_INSTALLED: |
| 344 | // ts('Installed'); |
| 345 | $extensionRow['statusLabel'] = ts('Enabled'); |
| 346 | break; |
| 347 | |
| 348 | case CRM_Extension_Manager::STATUS_DISABLED_MISSING: |
| 349 | $extensionRow['statusLabel'] = ts('Disabled (Missing)'); |
| 350 | break; |
| 351 | |
| 352 | case CRM_Extension_Manager::STATUS_INSTALLED_MISSING: |
| 353 | // ts('Installed'); |
| 354 | $extensionRow['statusLabel'] = ts('Enabled (Missing)'); |
| 355 | break; |
| 356 | |
| 357 | default: |
| 358 | $extensionRow['statusLabel'] = '(' . $extensionRow['status'] . ')'; |
| 359 | } |
| 360 | if ($manager->isIncompatible($obj->key)) { |
| 361 | $extensionRow['statusLabel'] = ts('Obsolete') . ($extensionRow['statusLabel'] ? (' - ' . $extensionRow['statusLabel']) : ''); |
| 362 | } |
| 363 | return $extensionRow; |
| 364 | } |
| 365 | |
| 366 | } |