CRM-16243 - Extension API - Manage extensions by path
authorTim Otten <totten@civicrm.org>
Sat, 26 Mar 2016 21:14:11 +0000 (14:14 -0700)
committerJohn Kirk <accounts@civifirst.com>
Mon, 9 Oct 2017 18:27:51 +0000 (18:27 +0000)
This allows one to enable or disable a series of extensions by path.

This should be useful, for example, when integrating with `composer` or
`drush make`. Without any knowledge of the specific extensions
involved, one might:

 * If you download a bunch of extensions to a common dir (e.g.
   composer's `vendor/`) and need to enable them all, then run
   `cv api extension.install path=$PWD/vendor/*` (circa `post-install-cmd`)
 * If you're deleting a specific directory (e.g. via composer's
   `uninstall`), then remove it gracefully from the DB by running
   `cv api extension.disable path=$PKGDIR` (circa `pre-package-uninstall`)

CRM/Extension/Mapper.php
api/v3/Extension.php
tests/phpunit/CRM/Extension/MapperTest.php

index 74fb01ab920432c409cbf1c43d50a2c848bb558e..12f624529b6a88902f234382f9280a93799f56fb 100644 (file)
@@ -340,6 +340,41 @@ class CRM_Extension_Mapper {
     return $urls;
   }
 
+  /**
+   * Get a list of extension keys, filtered by the corresponding file path.
+   *
+   * @param string $pattern
+   *   A file path. To search subdirectories, append "*".
+   *   Ex: "/var/www/extensions/*"
+   *   Ex: "/var/www/extensions/org.foo.bar"
+   * @return array
+   *   Array(string $key).
+   *   Ex: array("org.foo.bar").
+   */
+  public function getKeysByPath($pattern) {
+    $keys = array();
+
+    if (CRM_Utils_String::endsWith($pattern, '*')) {
+      $prefix = rtrim($pattern, '*');
+      foreach ($this->container->getKeys() as $key) {
+        $path = CRM_Utils_File::addTrailingSlash($this->container->getPath($key));
+        if (realpath($prefix) == realpath($path) || CRM_Utils_File::isChildPath($prefix, $path)) {
+          $keys[] = $key;
+        }
+      }
+    }
+    else {
+      foreach ($this->container->getKeys() as $key) {
+        $path = CRM_Utils_File::addTrailingSlash($this->container->getPath($key));
+        if (realpath($pattern) == realpath($path)) {
+          $keys[] = $key;
+        }
+      }
+    }
+
+    return $keys;
+  }
+
   /**
    * @return array
    *   Ex: $result['org.civicrm.foobar'] = new CRM_Extension_Info(...).
index aa0e38d91212a3b20b8993c1263ade61431aeb15..9e5fcf1c2eda6c48201746caa70ebf85ff561461 100644 (file)
@@ -41,6 +41,7 @@ define('API_V3_EXTENSION_DELIMITER', ',');
  *   Input parameters.
  *    - key: string, eg "com.example.myextension"
  *    - keys: array of string, eg array("com.example.myextension1", "com.example.myextension2")
+ *    - path: string, e.g. "/var/www/extensions/*"
  *
  * Using 'keys' should be more performant than making multiple API calls with 'key'
  *
@@ -70,11 +71,15 @@ function civicrm_api3_extension_install($params) {
 function _civicrm_api3_extension_install_spec(&$fields) {
   $fields['keys'] = array(
     'title' => 'Extension Key(s)',
-    'api.required' => 1,
     'api.aliases' => array('key'),
     'type' => CRM_Utils_Type::T_STRING,
     'description' => 'Fully qualified name of one or more extensions',
   );
+  $fields['path'] = array(
+    'title' => 'Extension Path',
+    'type' => CRM_Utils_Type::T_STRING,
+    'description' => 'The path to the extension. May use wildcard ("*").',
+  );
 }
 
 /**
@@ -114,6 +119,7 @@ function civicrm_api3_extension_upgrade() {
  *   Input parameters.
  *    - key: string, eg "com.example.myextension"
  *    - keys: array of string, eg array("com.example.myextension1", "com.example.myextension2")
+ *    - path: string, e.g. "/var/www/vendor/foo/myext" or "/var/www/vendor/*"
  *
  * Using 'keys' should be more performant than making multiple API calls with 'key'
  *
@@ -145,6 +151,7 @@ function _civicrm_api3_extension_enable_spec(&$fields) {
  *   Input parameters.
  *    - key: string, eg "com.example.myextension"
  *    - keys: array of string, eg array("com.example.myextension1", "com.example.myextension2")
+ *    - path: string, e.g. "/var/www/vendor/foo/myext" or "/var/www/vendor/*"
  *
  * Using 'keys' should be more performant than making multiple API calls with 'key'
  *
@@ -175,6 +182,7 @@ function _civicrm_api3_extension_disable_spec(&$fields) {
  *   Input parameters.
  *    - key: string, eg "com.example.myextension"
  *    - keys: array of string, eg array("com.example.myextension1", "com.example.myextension2")
+ *    - path: string, e.g. "/var/www/vendor/foo/myext" or "/var/www/vendor/*"
  *
  * Using 'keys' should be more performant than making multiple API calls with 'key'
  *
@@ -394,11 +402,16 @@ function civicrm_api3_extension_getremote($params) {
  *
  * @param array $params
  * @param string $key
- *   API request params with 'keys'.
+ *   API request params with 'keys' or 'path'.
+ *   - keys: A comma-delimited list of extension names
+ *   - path: An absolute directory path. May append '*' to match all sub-directories.
  *
  * @return array
  */
 function _civicrm_api3_getKeys($params, $key = 'keys') {
+  if ($key == 'path') {
+    return CRM_Extension_System::singleton()->getMapper()->getKeysByPath($params['path']);
+  }
   if (isset($params[$key])) {
     if (is_array($params[$key])) {
       return $params[$key];
@@ -408,7 +421,5 @@ function _civicrm_api3_getKeys($params, $key = 'keys') {
     }
     return explode(API_V3_EXTENSION_DELIMITER, $params[$key]);
   }
-  else {
-    return array();
-  }
+  throw new API_Exception("Missing required parameter: key, keys, or path");
 }
index c84b53e6a1fce64ad51000164cdb36517ca142d8..748d6ab16e90c186785f1b0ab4faea30c39099bd 100644 (file)
@@ -5,6 +5,22 @@
  * @group headless
  */
 class CRM_Extension_MapperTest extends CiviUnitTestCase {
+
+  /**
+   * @var string
+   */
+  protected $basedir, $basedir2;
+
+  /**
+   * @var CRM_Extension_Container_Interface
+   */
+  protected $container, $containerWithSlash;
+
+  /**
+   * @var CRM_Extension_Mapper
+   */
+  protected $mapper, $mapperWithSlash;
+
   public function setUp() {
     parent::setUp();
     list ($this->basedir, $this->container) = $this->_createContainer();
@@ -72,6 +88,27 @@ class CRM_Extension_MapperTest extends CiviUnitTestCase {
     $this->assertEquals(rtrim($config->resourceBase, '/'), $this->mapperWithSlash->keyToUrl('civicrm'));
   }
 
+  public function testGetKeysByPath() {
+    $mappers = array(
+      $this->basedir => $this->mapper,
+      $this->basedir2 => $this->mapperWithSlash,
+    );
+    foreach ($mappers as $basedir => $mapper) {
+      /** @var CRM_Extension_Mapper $mapper */
+      $this->assertEquals(array(), $mapper->getKeysByPath($basedir));
+      $this->assertEquals(array(), $mapper->getKeysByPath($basedir . '/weird'));
+      $this->assertEquals(array(), $mapper->getKeysByPath($basedir . '/weird/'));
+      $this->assertEquals(array(), $mapper->getKeysByPath($basedir . '/weird//'));
+      $this->assertEquals(array('test.foo.bar'), $mapper->getKeysByPath($basedir . '/*'));
+      $this->assertEquals(array('test.foo.bar'), $mapper->getKeysByPath($basedir . '//*'));
+      $this->assertEquals(array('test.foo.bar'), $mapper->getKeysByPath($basedir . '/weird/*'));
+      $this->assertEquals(array('test.foo.bar'), $mapper->getKeysByPath($basedir . '/weird/foobar'));
+      $this->assertEquals(array('test.foo.bar'), $mapper->getKeysByPath($basedir . '/weird/foobar/'));
+      $this->assertEquals(array('test.foo.bar'), $mapper->getKeysByPath($basedir . '/weird/foobar//'));
+      $this->assertEquals(array('test.foo.bar'), $mapper->getKeysByPath($basedir . '/weird/foobar/*'));
+    }
+  }
+
   /**
    * @param CRM_Utils_Cache_Interface $cache
    * @param null $cacheKey