Skip to content

Commit

Permalink
CRM-16243 - Extension API - Manage extensions by path
Browse files Browse the repository at this point in the history
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`)
  • Loading branch information
totten authored and JohnFF committed Oct 9, 2017
1 parent 34ba82e commit 3b3f6d2
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 5 deletions.
35 changes: 35 additions & 0 deletions CRM/Extension/Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,41 @@ public function getActiveModuleUrls() {
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(...).
Expand Down
21 changes: 16 additions & 5 deletions api/v3/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
* 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'
*
Expand Down Expand Up @@ -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 ("*").',
);
}

/**
Expand Down Expand Up @@ -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'
*
Expand Down Expand Up @@ -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'
*
Expand Down Expand Up @@ -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'
*
Expand Down Expand Up @@ -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];
Expand All @@ -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");
}
37 changes: 37 additions & 0 deletions tests/phpunit/CRM/Extension/MapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -72,6 +88,27 @@ public function testKeyToUrl() {
$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
Expand Down

0 comments on commit 3b3f6d2

Please sign in to comment.