Proof-of-concept - File scanner
authorTim Otten <totten@civicrm.org>
Wed, 13 Jun 2018 00:47:23 +0000 (17:47 -0700)
committerCiviCRM <info@civicrm.org>
Wed, 16 Sep 2020 02:13:16 +0000 (19:13 -0700)
ext/afform/CRM/Afform/AfformScanner.php [new file with mode: 0644]

diff --git a/ext/afform/CRM/Afform/AfformScanner.php b/ext/afform/CRM/Afform/AfformScanner.php
new file mode 100644 (file)
index 0000000..d15cc85
--- /dev/null
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * Class CRM_Afform_AfformScanner
+ *
+ * The AfformScanner searches the extensions and `civicrm.files` for subfolders
+ * named `afform`. Each item in there is interpreted as a form instance.
+ *
+ * To reduce file-scanning, we keep a cache of file paths.
+ */
+class CRM_Afform_AfformScanner {
+
+  const METADATA_FILE = 'meta.json';
+
+  const DEFAULT_REQUIRES = 'afformCore';
+
+  /**
+   * @var CRM_Utils_Cache_Interface
+   */
+  protected $cache;
+
+  /**
+   * CRM_Afform_AfformScanner constructor.
+   */
+  public function __construct() {
+    // TODO Manage this is a service, and inject the cache service.
+    $this->cache = new CRM_Utils_Cache_SqlGroup([
+      'group' => md5('afform_' . CRM_Core_Config_Runtime::getId() . $this->getSiteLocalPath()),
+      'prefetch' => FALSE,
+    ]);
+  }
+
+  /**
+   * @return array
+   *   Ex: ['view-individual' => ['/var/www/foo/afform/view-individual']]
+   */
+  public function findFilePaths() {
+    if (!CRM_Core_Config::singleton()->debug) {
+      // FIXME: Use a separate setting. Maybe use the asset-builder cache setting?
+      $paths = $this->cache->get('allPaths');
+      if ($paths !== NULL) {
+        return $paths;
+      }
+    }
+
+    $paths = array();
+
+    $mapper = CRM_Extension_System::singleton()->getMapper();
+    foreach ($mapper->getModules() as $module) {
+      /** @var $module CRM_Core_Module */
+      if ($module->is_active) {
+        $this->appendFilePaths($paths, dirname($mapper->keyToPath($module->name)) . DIRECTORY_SEPARATOR . 'afform', 20);
+      }
+    }
+
+    $this->appendFilePaths($paths, $this->getSiteLocalPath(), 10);
+
+    $this->cache->set('allPaths', $paths);
+    return $paths;
+  }
+
+  /**
+   * Get the full path to the given file.
+   *
+   * @param string $formName
+   *   Ex: 'view-individual'
+   * @param string $subFile
+   *   Ex: 'meta.json'
+   * @return string|NULL
+   *   Ex: '/var/www/sites/default/files/civicrm/afform/view-individual'
+   */
+  public function findFilePath($formName, $subFile) {
+    $paths = $this->findFilePaths();
+
+    if (isset($paths[$formName])) {
+      foreach ($paths[$formName] as $path) {
+        if (file_exists($path . DIRECTORY_SEPARATOR . $subFile)) {
+          return $path . DIRECTORY_SEPARATOR . $subFile;
+        }
+      }
+    }
+
+    return NULL;
+  }
+
+  /**
+   * Determine the path where we can write our own customized/overriden
+   * version of a file.
+   *
+   * @param string $formName
+   *   Ex: 'view-individual'
+   * @param string $file
+   *   Ex: 'meta.json'
+   * @return string|NULL
+   *   Ex: '/var/www/sites/default/files/civicrm/afform/view-individual'
+   */
+  public function createSiteLocalPath($formName, $file) {
+    return $this->getSiteLocalPath() . DIRECTORY_SEPARATOR . $formName . DIRECTORY_SEPARATOR . $file;
+  }
+
+  public function clear() {
+    $this->cache->flush();
+  }
+
+  /**
+   * Get the effective metadata for a form.
+   *
+   * @param string $name
+   *   Ex: 'view-individual'
+   * @return array
+   *   An array with some mix of the following keys: name, title, description, route, requires
+   *   Ex: [
+   *     'name' => 'view-individual',
+   *     'title' => 'View an individual contact',
+   *     'route' => 'civicrm/view-individual',
+   *     'requires' => ['afform'],
+   *   ]
+   */
+  public function getMeta($name) {
+    // FIXME error checking
+    $metaFile = $this->findFilePath($name, self::METADATA_FILE);
+    if (!$metaFile) {
+      return NULL;
+    }
+
+    $defaults = [
+      'name' => $name,
+      'requires' => explode(',', self::DEFAULT_REQUIRES),
+      'title' => '',
+      'description' => '',
+    ];
+
+    return array_merge($defaults, json_decode(file_get_contents($metaFile), 1));
+  }
+
+  /**
+   * @param array $formPaths
+   *   List of all form paths.
+   *   Ex: ['foo' => [0 => '/var/www/org.example.foobar/afform//foo']]
+   * @param string $parent
+   *   Ex: '/var/www/org.example.foobar/afform/'
+   * @param int $priority
+   *   Lower priority files override higher priority files.
+   */
+  private function appendFilePaths(&$formPaths, $parent, $priority) {
+    $parent = CRM_Utils_File::addTrailingSlash($parent);
+    if (is_dir($parent) && $handle = opendir($parent)) {
+      while (FALSE !== ($entry = readdir($handle))) {
+        if ($entry{0} !== '.' && is_dir($parent . $entry)) {
+          $formPaths[$entry][$priority] = $parent . $entry;
+          ksort($formPaths[$entry]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Get the path where site-local form customizations are stored.
+   *
+   * @return mixed|string
+   *   Ex: '/var/www/sites/default/files/civicrm/afform'.
+   */
+  private function getSiteLocalPath() {
+    // TODO Allow a setting override.
+    // return Civi::paths()->getPath(Civi::settings()->get('afformPath'));
+    return Civi::paths()->getPath('[civicrm.files]/afform');
+  }
+
+}