fix missing newly created activity types
[civicrm-core.git] / CRM / Extension / Mapper.php
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 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
29 * @copyright CiviCRM LLC https://civicrm.org/licensing
30 */
31 class CRM_Extension_Mapper {
32
33 /**
34 * An URL for public extensions repository.
35 */
36
37 /**
38 * Extension info file name.
39 */
40 const EXT_TEMPLATES_DIRNAME = 'templates';
41
42 /**
43 * @var CRM_Extension_Container_Interface
44 */
45 protected $container;
46
47 /**
48 * @var \CRM_Extension_Info[]
49 * (key => CRM_Extension_Info)
50 */
51 protected $infos = [];
52
53 /**
54 * @var array
55 */
56 protected $moduleExtensions = NULL;
57
58 /**
59 * @var CRM_Utils_Cache_Interface
60 */
61 protected $cache;
62
63 protected $cacheKey;
64
65 protected $civicrmPath;
66
67 protected $civicrmUrl;
68
69 /**
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
75 */
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;
80 if ($civicrmUrl) {
81 $this->civicrmUrl = rtrim($civicrmUrl, '/');
82 }
83 else {
84 $config = CRM_Core_Config::singleton();
85 $this->civicrmUrl = rtrim($config->resourceBase, '/');
86 }
87 if ($civicrmPath) {
88 $this->civicrmPath = rtrim($civicrmPath, '/');
89 }
90 else {
91 global $civicrm_root;
92 $this->civicrmPath = rtrim($civicrm_root, '/');
93 }
94 }
95
96 /**
97 * Given the class, provides extension's key.
98 *
99 *
100 * @param string $clazz
101 * Extension class name.
102 *
103 * @return string
104 * name of extension key
105 */
106 public function classToKey($clazz) {
107 return str_replace('_', '.', $clazz);
108 }
109
110 /**
111 * Given the class, provides extension path.
112 *
113 *
114 * @param $clazz
115 *
116 * @return string
117 * full path the extension .php file
118 */
119 public function classToPath($clazz) {
120 $elements = explode('_', $clazz);
121 $key = implode('.', $elements);
122 return $this->keyToPath($key);
123 }
124
125 /**
126 * Given the string, returns true or false if it's an extension key.
127 *
128 *
129 * @param string $key
130 * A string which might be an extension key.
131 *
132 * @return bool
133 * true if given string is an extension name
134 */
135 public function isExtensionKey($key) {
136 // check if the string is an extension name or the class
137 return (strpos($key, '.') !== FALSE) ? TRUE : FALSE;
138 }
139
140 /**
141 * Given the string, returns true or false if it's an extension class name.
142 *
143 *
144 * @param string $clazz
145 * A string which might be an extension class name.
146 *
147 * @return bool
148 * true if given string is an extension class name
149 */
150 public function isExtensionClass($clazz) {
151
152 if (substr($clazz, 0, 4) != 'CRM_') {
153 return (bool) preg_match('/^[a-z0-9]+(_[a-z0-9]+)+$/', $clazz);
154 }
155 return FALSE;
156 }
157
158 /**
159 * @param string $key
160 * Extension fully-qualified-name.
161 * @param bool $fresh
162 *
163 * @throws CRM_Extension_Exception
164 *
165 * @return CRM_Extension_Info
166 */
167 public function keyToInfo($key, $fresh = FALSE) {
168 if ($fresh || !array_key_exists($key, $this->infos)) {
169 try {
170 $this->infos[$key] = CRM_Extension_Info::loadFromFile($this->container->getPath($key) . DIRECTORY_SEPARATOR . CRM_Extension_Info::FILENAME);
171 }
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 $dbInfo = CRM_Extension_System::singleton()->getManager()->createInfoFromDB($key);
175 if (!$dbInfo) {
176 throw $e;
177 }
178 $this->infos[$key] = $dbInfo;
179 }
180 }
181 return $this->infos[$key];
182 }
183
184 /**
185 * Given the key, provides extension's class name.
186 *
187 *
188 * @param string $key
189 * Extension key.
190 *
191 * @return string
192 * name of extension's main class
193 */
194 public function keyToClass($key) {
195 return str_replace('.', '_', $key);
196 }
197
198 /**
199 * Given the key, provides the path to file containing
200 * extension's main class.
201 *
202 *
203 * @param string $key
204 * Extension key.
205 *
206 * @return string
207 * path to file containing extension's main class
208 */
209 public function keyToPath($key) {
210 $info = $this->keyToInfo($key);
211 return $this->container->getPath($key) . DIRECTORY_SEPARATOR . $info->file . '.php';
212 }
213
214 /**
215 * Given the key, provides the path to file containing
216 * extension's main class.
217 *
218 * @param string $key
219 * Extension key.
220 * @return string
221 * local path of the extension source tree
222 */
223 public function keyToBasePath($key) {
224 if ($key == 'civicrm') {
225 return $this->civicrmPath;
226 }
227 return $this->container->getPath($key);
228 }
229
230 /**
231 * Given the key, provides the path to file containing
232 * extension's main class.
233 *
234 *
235 * @param string $key
236 * Extension key.
237 *
238 * @return string
239 * url for resources in this extension
240 *
241 * @throws \CRM_Extension_Exception_MissingException
242 */
243 public function keyToUrl($key) {
244 if ($key === 'civicrm') {
245 // CRM-12130 Workaround: If the domain's config_backend is NULL at the start of the request,
246 // then the Mapper is wrongly constructed with an empty value for $this->civicrmUrl.
247 if (empty($this->civicrmUrl)) {
248 $config = CRM_Core_Config::singleton();
249 return rtrim($config->resourceBase, '/');
250 }
251 return $this->civicrmUrl;
252 }
253
254 return $this->container->getResUrl($key);
255 }
256
257 /**
258 * Fetch the list of active extensions of type 'module'
259 *
260 * @param bool $fresh
261 * whether to forcibly reload extensions list from canonical store.
262 * @return array
263 * array(array('prefix' => $, 'fullName' => $, 'filePath' => $))
264 */
265 public function getActiveModuleFiles($fresh = FALSE) {
266 if (!defined('CIVICRM_DSN')) {
267 // hmm, ok
268 return [];
269 }
270
271 // The list of module files is cached in two tiers. The tiers are slightly
272 // different:
273 //
274 // 1. The persistent tier (cache) stores
275 // names WITHOUT absolute paths.
276 // 2. The ephemeral/thread-local tier (statics) stores names
277 // WITH absolute paths.
278 // Return static value instead of re-running query
279 if (isset(Civi::$statics[__CLASS__]['moduleExtensions']) && !$fresh) {
280 return Civi::$statics[__CLASS__]['moduleExtensions'];
281 }
282
283 $moduleExtensions = NULL;
284
285 // Checked if it's stored in the persistent cache.
286 if ($this->cache && !$fresh) {
287 $moduleExtensions = $this->cache->get($this->cacheKey . '_moduleFiles');
288 }
289
290 // If cache is empty we build it from database.
291 if (!is_array($moduleExtensions)) {
292 $compat = CRM_Extension_System::getCompatibilityInfo();
293
294 // Check canonical module list
295 $moduleExtensions = [];
296 $sql = '
297 SELECT full_name, file
298 FROM civicrm_extension
299 WHERE is_active = 1
300 AND type = "module"
301 ';
302 $dao = CRM_Core_DAO::executeQuery($sql);
303 while ($dao->fetch()) {
304 if (!empty($compat[$dao->full_name]['force-uninstall'])) {
305 continue;
306 }
307 $moduleExtensions[] = [
308 'prefix' => $dao->file,
309 'fullName' => $dao->full_name,
310 'filePath' => NULL,
311 ];
312 }
313
314 if ($this->cache) {
315 $this->cache->set($this->cacheKey . '_moduleFiles', $moduleExtensions);
316 }
317 }
318
319 // Since we're not caching the full path we add it now.
320 array_walk($moduleExtensions, function(&$value, $key) {
321 try {
322 if (!$value['filePath']) {
323 $value['filePath'] = $this->keyToPath($value['fullName']);
324 }
325 }
326 catch (CRM_Extension_Exception $e) {
327 // Putting a stub here provides more consistency
328 // in how getActiveModuleFiles when racing between
329 // dirty file-removals and cache-clears.
330 CRM_Core_Session::setStatus($e->getMessage(), '', 'error');
331 $value['filePath'] = NULL;
332 }
333 });
334
335 Civi::$statics[__CLASS__]['moduleExtensions'] = $moduleExtensions;
336
337 return $moduleExtensions;
338 }
339
340 /**
341 * Get a list of base URLs for all active modules.
342 *
343 * @return array
344 * (string $extKey => string $baseUrl)
345 *
346 * @throws \CRM_Extension_Exception_MissingException
347 */
348 public function getActiveModuleUrls() {
349 // TODO optimization/caching
350 $urls = [];
351 $urls['civicrm'] = $this->keyToUrl('civicrm');
352 foreach ($this->getModules() as $module) {
353 /** @var $module CRM_Core_Module */
354 if ($module->is_active) {
355 try {
356 $urls[$module->name] = $this->keyToUrl($module->name);
357 }
358 catch (CRM_Extension_Exception_MissingException $e) {
359 CRM_Core_Session::setStatus(ts('An enabled extension is missing from the extensions directory') . ':' . $module->name);
360 }
361 }
362 }
363 return $urls;
364 }
365
366 /**
367 * Get a list of extension keys, filtered by the corresponding file path.
368 *
369 * @param string $pattern
370 * A file path. To search subdirectories, append "*".
371 * Ex: "/var/www/extensions/*"
372 * Ex: "/var/www/extensions/org.foo.bar"
373 * @return array
374 * Array(string $key).
375 * Ex: array("org.foo.bar").
376 */
377 public function getKeysByPath($pattern) {
378 $keys = [];
379
380 if (CRM_Utils_String::endsWith($pattern, '*')) {
381 $prefix = rtrim($pattern, '*');
382 foreach ($this->container->getKeys() as $key) {
383 $path = CRM_Utils_File::addTrailingSlash($this->container->getPath($key));
384 if (realpath($prefix) == realpath($path) || CRM_Utils_File::isChildPath($prefix, $path)) {
385 $keys[] = $key;
386 }
387 }
388 }
389 else {
390 foreach ($this->container->getKeys() as $key) {
391 $path = CRM_Utils_File::addTrailingSlash($this->container->getPath($key));
392 if (realpath($pattern) == realpath($path)) {
393 $keys[] = $key;
394 }
395 }
396 }
397
398 return $keys;
399 }
400
401 /**
402 * Get a list of extensions which match a given tag.
403 *
404 * @param string $tag
405 * Ex: 'foo'
406 * @return array
407 * Array(string $key).
408 * Ex: array("org.foo.bar").
409 */
410 public function getKeysByTag($tag) {
411 $allTags = $this->getAllTags();
412 return $allTags[$tag] ?? [];
413 }
414
415 /**
416 * Get a list of extension tags.
417 *
418 * @return array
419 * Ex: ['form-building' => ['org.civicrm.afform-gui', 'org.civicrm.afform-html']]
420 */
421 public function getAllTags() {
422 $tags = Civi::cache('short')->get('extension_tags', NULL);
423 if ($tags !== NULL) {
424 return $tags;
425 }
426
427 $tags = [];
428 $allInfos = $this->getAllInfos();
429 foreach ($allInfos as $key => $info) {
430 foreach ($info->tags as $tag) {
431 $tags[$tag][] = $key;
432 }
433 }
434 return $tags;
435 }
436
437 /**
438 * @return array
439 * Ex: $result['org.civicrm.foobar'] = new CRM_Extension_Info(...).
440 * @throws \CRM_Extension_Exception
441 * @throws \Exception
442 */
443 public function getAllInfos() {
444 foreach ($this->container->getKeys() as $key) {
445 try {
446 $this->keyToInfo($key);
447 }
448 catch (CRM_Extension_Exception_ParseException $e) {
449 CRM_Core_Session::setStatus(ts('Parse error in extension: %1', [
450 1 => $e->getMessage(),
451 ]), '', 'error');
452 CRM_Core_Error::debug_log_message("Parse error in extension: " . $e->getMessage());
453 continue;
454 }
455 }
456 return $this->infos;
457 }
458
459 /**
460 * @param string $name
461 *
462 * @return bool
463 */
464 public function isActiveModule($name) {
465 $activeModules = $this->getActiveModuleFiles();
466 foreach ($activeModules as $activeModule) {
467 if ($activeModule['prefix'] == $name) {
468 return TRUE;
469 }
470 }
471 return FALSE;
472 }
473
474 /**
475 * Get a list of all installed modules, including enabled and disabled ones
476 *
477 * @return array
478 * CRM_Core_Module
479 */
480 public function getModules() {
481 $result = [];
482 $dao = new CRM_Core_DAO_Extension();
483 $dao->type = 'module';
484 $dao->find();
485 while ($dao->fetch()) {
486 $result[] = new CRM_Core_Module($dao->full_name, $dao->is_active);
487 }
488 return $result;
489 }
490
491 /**
492 * Given the class, provides the template path.
493 *
494 *
495 * @param string $clazz
496 * Extension class name.
497 *
498 * @return string
499 * path to extension's templates directory
500 */
501 public function getTemplatePath($clazz) {
502 $path = $this->container->getPath($this->classToKey($clazz));
503 return $path . DIRECTORY_SEPARATOR . self::EXT_TEMPLATES_DIRNAME;
504 /*
505 $path = $this->classToPath($clazz);
506 $pathElm = explode(DIRECTORY_SEPARATOR, $path);
507 array_pop($pathElm);
508 return implode(DIRECTORY_SEPARATOR, $pathElm) . DIRECTORY_SEPARATOR . self::EXT_TEMPLATES_DIRNAME;
509 */
510 }
511
512 /**
513 * Given te class, provides the template name.
514 * @todo consider multiple templates, support for one template for now
515 *
516 *
517 * @param string $clazz
518 * Extension class name.
519 *
520 * @return string
521 * extension's template name
522 */
523 public function getTemplateName($clazz) {
524 $info = $this->keyToInfo($this->classToKey($clazz));
525 return (string) $info->file . '.tpl';
526 }
527
528 public function refresh() {
529 $this->infos = [];
530 $this->moduleExtensions = NULL;
531 if ($this->cache) {
532 $this->cache->delete($this->cacheKey . '_moduleFiles');
533 }
534 // FIXME: How can code so code wrong be so right?
535 CRM_Extension_System::singleton()->getClassLoader()->refresh();
536 }
537
538 }