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