Merge pull request #17020 from eileenmcnaughton/dedupe1
[civicrm-core.git] / CRM / Extension / Mapper.php
index a21af4d43b7e2c7458014fd216043fac14abbc5c..ce50f74495d3a000f28f9cadcaf7fc39c13acb4a 100644 (file)
@@ -161,7 +161,7 @@ class CRM_Extension_Mapper {
    * @param bool $fresh
    *
    * @throws CRM_Extension_Exception
-   * @throws Exception
+   *
    * @return CRM_Extension_Info
    */
   public function keyToInfo($key, $fresh = FALSE) {
@@ -171,10 +171,11 @@ class CRM_Extension_Mapper {
       }
       catch (CRM_Extension_Exception $e) {
         // file has more detailed info, but we'll fallback to DB if it's missing -- DB has enough info to uninstall
-        $this->infos[$key] = CRM_Extension_System::singleton()->getManager()->createInfoFromDB($key);
-        if (!$this->infos[$key]) {
+        $dbInfo = CRM_Extension_System::singleton()->getManager()->createInfoFromDB($key);
+        if (!$dbInfo) {
           throw $e;
         }
+        $this->infos[$key] = $dbInfo;
       }
     }
     return $this->infos[$key];
@@ -236,9 +237,11 @@ class CRM_Extension_Mapper {
    *
    * @return string
    *   url for resources in this extension
+   *
+   * @throws \CRM_Extension_Exception_MissingException
    */
   public function keyToUrl($key) {
-    if ($key == 'civicrm') {
+    if ($key === 'civicrm') {
       // CRM-12130 Workaround: If the domain's config_backend is NULL at the start of the request,
       // then the Mapper is wrongly constructed with an empty value for $this->civicrmUrl.
       if (empty($this->civicrmUrl)) {
@@ -257,7 +260,7 @@ class CRM_Extension_Mapper {
    * @param bool $fresh
    *   whether to forcibly reload extensions list from canonical store.
    * @return array
-   *   array(array('prefix' => $, 'file' => $))
+   *   array(array('prefix' => $, 'fullName' => $, 'filePath' => $))
    */
   public function getActiveModuleFiles($fresh = FALSE) {
     if (!defined('CIVICRM_DSN')) {
@@ -265,11 +268,26 @@ class CRM_Extension_Mapper {
       return [];
     }
 
+    // The list of module files is cached in two tiers. The tiers are slightly
+    // different:
+    //
+    // 1. The persistent tier (cache) stores
+    // names WITHOUT absolute paths.
+    // 2. The ephemeral/thread-local tier (statics) stores names
+    // WITH absolute paths.
+    // Return static value instead of re-running query
+    if (isset(Civi::$statics[__CLASS__]['moduleExtensions']) && !$fresh) {
+      return Civi::$statics[__CLASS__]['moduleExtensions'];
+    }
+
     $moduleExtensions = NULL;
+
+    // Checked if it's stored in the persistent cache.
     if ($this->cache && !$fresh) {
       $moduleExtensions = $this->cache->get($this->cacheKey . '_moduleFiles');
     }
 
+    // If cache is empty we build it from database.
     if (!is_array($moduleExtensions)) {
       $compat = CRM_Extension_System::getCompatibilityInfo();
 
@@ -286,28 +304,36 @@ class CRM_Extension_Mapper {
         if (!empty($compat[$dao->full_name]['force-uninstall'])) {
           continue;
         }
-        try {
-          $moduleExtensions[] = [
-            'prefix' => $dao->file,
-            'filePath' => $this->keyToPath($dao->full_name),
-          ];
-        }
-        catch (CRM_Extension_Exception $e) {
-          // Putting a stub here provides more consistency
-          // in how getActiveModuleFiles when racing between
-          // dirty file-removals and cache-clears.
-          CRM_Core_Session::setStatus($e->getMessage(), '', 'error');
-          $moduleExtensions[] = [
-            'prefix' => $dao->file,
-            'filePath' => NULL,
-          ];
-        }
+        $moduleExtensions[] = [
+          'prefix' => $dao->file,
+          'fullName' => $dao->full_name,
+          'filePath' => NULL,
+        ];
       }
 
       if ($this->cache) {
         $this->cache->set($this->cacheKey . '_moduleFiles', $moduleExtensions);
       }
     }
+
+    // Since we're not caching the full path we add it now.
+    array_walk($moduleExtensions, function(&$value, $key) {
+      try {
+        if (!$value['filePath']) {
+          $value['filePath'] = $this->keyToPath($value['fullName']);
+        }
+      }
+      catch (CRM_Extension_Exception $e) {
+        // Putting a stub here provides more consistency
+        // in how getActiveModuleFiles when racing between
+        // dirty file-removals and cache-clears.
+        CRM_Core_Session::setStatus($e->getMessage(), '', 'error');
+        $value['filePath'] = NULL;
+      }
+    });
+
+    Civi::$statics[__CLASS__]['moduleExtensions'] = $moduleExtensions;
+
     return $moduleExtensions;
   }
 
@@ -316,6 +342,8 @@ class CRM_Extension_Mapper {
    *
    * @return array
    *   (string $extKey => string $baseUrl)
+   *
+   * @throws \CRM_Extension_Exception_MissingException
    */
   public function getActiveModuleUrls() {
     // TODO optimization/caching
@@ -324,7 +352,12 @@ class CRM_Extension_Mapper {
     foreach ($this->getModules() as $module) {
       /** @var $module CRM_Core_Module */
       if ($module->is_active) {
-        $urls[$module->name] = $this->keyToUrl($module->name);
+        try {
+          $urls[$module->name] = $this->keyToUrl($module->name);
+        }
+        catch (CRM_Extension_Exception_MissingException $e) {
+          CRM_Core_Session::setStatus(ts('An enabled extension is missing from the extensions directory') . ':' . $module->name);
+        }
       }
     }
     return $urls;
@@ -365,6 +398,42 @@ class CRM_Extension_Mapper {
     return $keys;
   }
 
+  /**
+   * Get a list of extensions which match a given tag.
+   *
+   * @param string $tag
+   *   Ex: 'foo'
+   * @return array
+   *   Array(string $key).
+   *   Ex: array("org.foo.bar").
+   */
+  public function getKeysByTag($tag) {
+    $allTags = $this->getAllTags();
+    return $allTags[$tag] ?? [];
+  }
+
+  /**
+   * Get a list of extension tags.
+   *
+   * @return array
+   *   Ex: ['form-building' => ['org.civicrm.afform-gui', 'org.civicrm.afform-html']]
+   */
+  public function getAllTags() {
+    $tags = Civi::cache('short')->get('extension_tags', NULL);
+    if ($tags !== NULL) {
+      return $tags;
+    }
+
+    $tags = [];
+    $allInfos = $this->getAllInfos();
+    foreach ($allInfos as $key => $info) {
+      foreach ($info->tags as $tag) {
+        $tags[$tag][] = $key;
+      }
+    }
+    return $tags;
+  }
+
   /**
    * @return array
    *   Ex: $result['org.civicrm.foobar'] = new CRM_Extension_Info(...).
@@ -373,7 +442,16 @@ class CRM_Extension_Mapper {
    */
   public function getAllInfos() {
     foreach ($this->container->getKeys() as $key) {
-      $this->keyToInfo($key);
+      try {
+        $this->keyToInfo($key);
+      }
+      catch (CRM_Extension_Exception_ParseException $e) {
+        CRM_Core_Session::setStatus(ts('Parse error in extension: %1', [
+          1 => $e->getMessage(),
+        ]), '', 'error');
+        CRM_Core_Error::debug_log_message("Parse error in extension: " . $e->getMessage());
+        continue;
+      }
     }
     return $this->infos;
   }