diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0795161 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.2 20190518 + +### Fixed + +- Missing backslash in regexp + +## 1.0.1 20171028 + +### Fixed + +- Unexpected empty element in 'plugins' option when all plugins are being removed +- Unexpected semicolon insertion + +### Review + +- CS fixes, code refactoring +- 'plugins' option configuration matching regexes + +## 1.0.0 + +### Changed + +- Assume 'yes' in non-interactive mode. + +### Fixed + +- Deprecation Notice: The Composer\Package\LinkConstraint\VersionConstraint class is deprecated... diff --git a/README.md b/README.md index f7713d8..4067e47 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# Plugin Installer for Roundcube +# i-MSCP Plugin Installer for Roundcube + +This installer is a drop-in replacement for the roundcube/plugin-installer. + +See the [CHANGELOG.md](CHANGELOG.md) file for list of changes. This installer ensures that plugins end up in the correct directory: @@ -29,7 +33,7 @@ This installer ensures that plugins end up in the correct directory: } ] "require": { - "roundcube/plugin-installer": "*" + "imscp/roundcube-plugin-installer": "^1.0" }, "minimum-stability": "dev-master" } diff --git a/composer.json b/composer.json index 45e6cab..31ea899 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "roundcube/plugin-installer", - "description": "A composer-installer for Roundcube plugins.", + "name": "imscp/roundcube-plugin-installer", + "description": "i-MSCP composer-installer for Roundcube plugins.", "type": "composer-installer", "license": "GPL-3.0+", "authors": [ @@ -11,6 +11,10 @@ { "name": "Till Klampaeckel", "email": "till@php.net" + }, + { + "name": "Laurent Declercq", + "email": "l.declercq@nuxwin.com" } ], "autoload": { @@ -25,9 +29,13 @@ "src/bin/rcubeinitdb.sh" ], "require": { - "php": ">=5.3.0" + "php": ">=5.3.0", + "composer-plugin-api": "^1.0" }, "require-dev": { - "composer/composer": "*" + "composer/composer": "^1.0.0-alpha11" + }, + "replace": { + "roundcube/plugin-installer": "0.1.*" } } diff --git a/src/Roundcube/Composer/PluginInstaller.php b/src/Roundcube/Composer/PluginInstaller.php index c02b399..4bdded1 100644 --- a/src/Roundcube/Composer/PluginInstaller.php +++ b/src/Roundcube/Composer/PluginInstaller.php @@ -3,10 +3,10 @@ namespace Roundcube\Composer; use Composer\Installer\LibraryInstaller; -use Composer\Package\Version\VersionParser; -use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\PackageInterface; +use Composer\Package\Version\VersionParser; use Composer\Repository\InstalledRepositoryInterface; +use Composer\Semver\Constraint\Constraint as VersionConstraint; use Composer\Util\ProcessExecutor; /** @@ -14,9 +14,9 @@ * @package PluginInstaller * @author Till Klampaeckel * @author Thomas Bruederli + * @author Laurent Declercq * @license GPL-3.0+ - * @version GIT: - * @link http://github.com/roundcube/plugin-installer + * @link https://github.com/i-MSCP/plugin-installer */ class PluginInstaller extends LibraryInstaller { @@ -28,7 +28,8 @@ class PluginInstaller extends LibraryInstaller public function getInstallPath(PackageInterface $package) { static $vendorDir; - if ($vendorDir === null) { + + if ($vendorDir === NULL) { $vendorDir = $this->getVendorDir(); } @@ -44,31 +45,38 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa parent::install($repo, $package); // post-install: activate plugin in Roundcube config - $config_file = $this->rcubeConfigFile(); - $plugin_name = $this->getPluginName($package); - $plugin_dir = $this->getVendorDir() . DIRECTORY_SEPARATOR . $plugin_name; + $configFile = $this->rcubeConfigFile(); + $pluginName = $this->getPluginName($package); + $pluginDir = $this->getVendorDir() . DIRECTORY_SEPARATOR . $pluginName; $extra = $package->getExtra(); - $plugin_name = $this->getPluginName($package); - - if (is_writeable($config_file) && php_sapi_name() == 'cli') { - $answer = $this->io->askConfirmation("Do you want to activate the plugin $plugin_name? [N|y] ", false); + $pluginName = $this->getPluginName($package); + + if (is_writeable($configFile) + && php_sapi_name() == 'cli' + ) { + $answer = $this->io->askConfirmation( + "Do you want to activate the $pluginName plugin for the i-MSCP Roundcube Webmail Suite? [n|Y] ", true + ); if (true === $answer) { - $this->rcubeAlterConfig($plugin_name, true); + $this->rcubeAlterConfig($pluginName, true); } } // copy config.inc.php.dist -> config.inc.php - if (is_file($plugin_dir . DIRECTORY_SEPARATOR . 'config.inc.php.dist') && !is_file($plugin_dir . DIRECTORY_SEPARATOR . 'config.inc.php') && is_writeable($plugin_dir)) { + if (is_file($pluginDir . DIRECTORY_SEPARATOR . 'config.inc.php.dist') + && !is_file($pluginDir . DIRECTORY_SEPARATOR . 'config.inc.php') + && is_writeable($pluginDir) + ) { $this->io->write("Creating plugin config file"); - copy($plugin_dir . DIRECTORY_SEPARATOR . 'config.inc.php.dist', $plugin_dir . DIRECTORY_SEPARATOR . 'config.inc.php'); + copy($pluginDir . DIRECTORY_SEPARATOR . 'config.inc.php.dist', $pluginDir . DIRECTORY_SEPARATOR . 'config.inc.php'); } // initialize database schema - if (!empty($extra['roundcube']['sql-dir'])) { - if ($sqldir = realpath($plugin_dir . DIRECTORY_SEPARATOR . $extra['roundcube']['sql-dir'])) { - $this->io->write("Running database initialization script for $plugin_name"); - system(getcwd() . "/vendor/bin/rcubeinitdb.sh --package=$plugin_name --dir=$sqldir"); - } + if (!empty($extra['roundcube']['sql-dir']) + && ($sqlDir = realpath($pluginDir . DIRECTORY_SEPARATOR . $extra['roundcube']['sql-dir'])) + ) { + $this->io->write("Running database initialization script for $pluginName"); + system(getcwd() . "/vendor/bin/rcubeinitdb.sh --package=$pluginName --dir=$sqlDir"); } // run post-install script @@ -84,17 +92,16 @@ public function update(InstalledRepositoryInterface $repo, PackageInterface $ini { $this->rcubeVersionCheck($target); parent::update($repo, $initial, $target); - $extra = $target->getExtra(); // trigger updatedb.sh if (!empty($extra['roundcube']['sql-dir'])) { - $plugin_name = $this->getPluginName($target); - $plugin_dir = $this->getVendorDir() . DIRECTORY_SEPARATOR . $plugin_name; + $pluginName = $this->getPluginName($target); + $pluginDir = $this->getVendorDir() . DIRECTORY_SEPARATOR . $pluginName; - if ($sqldir = realpath($plugin_dir . DIRECTORY_SEPARATOR . $extra['roundcube']['sql-dir'])) { - $this->io->write("Updating database schema for $plugin_name"); - system(getcwd() . "/bin/updatedb.sh --package=$plugin_name --dir=$sqldir", $res); + if ($sqlDir = realpath($pluginDir . DIRECTORY_SEPARATOR . $extra['roundcube']['sql-dir'])) { + $this->io->write("Updating database schema for $pluginName"); + system(getcwd() . "/bin/updatedb.sh --package=$pluginName --dir=$sqlDir", $res); } } @@ -112,8 +119,8 @@ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $ parent::uninstall($repo, $package); // post-uninstall: deactivate plugin - $plugin_name = $this->getPluginName($package); - $this->rcubeAlterConfig($plugin_name, false); + $pluginName = $this->getPluginName($package); + $this->rcubeAlterConfig($pluginName, false); // run post-uninstall script $extra = $package->getExtra(); @@ -131,111 +138,139 @@ public function supports($packageType) } /** - * Setup vendor directory to one of these two: - * ./plugins + * Return vendor directory * * @return string */ public function getVendorDir() { - $pluginDir = getcwd(); - $pluginDir .= '/plugins'; - - return $pluginDir; + return getcwd() . '/plugins'; } /** * Extract the (valid) plugin name from the package object + * + * @param PackageInterface $package + * @return string */ private function getPluginName(PackageInterface $package) { - @list($vendor, $pluginName) = explode('/', $package->getPrettyName()); - + @list(, $pluginName) = explode('/', $package->getPrettyName()); return strtr($pluginName, '-', '_'); } /** * Check version requirements from the "extra" block of a package * against the local Roundcube version + * + * @throws \Exception + * @param PackageInterface $package + * @return void */ - private function rcubeVersionCheck($package) + private function rcubeVersionCheck(PackageInterface $package) { $parser = new VersionParser; // read rcube version from iniset - $rootdir = getcwd(); - $iniset = @file_get_contents($rootdir . '/program/include/iniset.php'); - if (preg_match('/define\(.RCMAIL_VERSION.,\s*.([0-9.]+[a-z-]*)?/', $iniset, $m)) { - $rcubeVersion = $parser->normalize(str_replace('-git', '.999', $m[1])); - } else { - throw new \Exception("Unable to find a Roundcube installation in $rootdir"); + $rootDir = getcwd(); + $iniSet = @file_get_contents($rootDir . '/program/include/iniset.php'); + + if (!preg_match('/define\(.RCMAIL_VERSION.,\s*.([0-9.]+[a-z-]*)?/', $iniSet, $m)) { + throw new \Exception("Unable to find a Roundcube installation in $rootDir"); } + $rcubeVersion = $parser->normalize(str_replace('-git', '.999', $m[1])); $extra = $package->getExtra(); - if (!empty($extra['roundcube'])) { - foreach (array('min-version' => '>=', 'max-version' => '<=') as $key => $operator) { - if (!empty($extra['roundcube'][$key])) { - $version = $parser->normalize(str_replace('-git', '.999', $extra['roundcube'][$key])); - $constraint = new VersionConstraint($operator, $version); - if (!$constraint->versionCompare($rcubeVersion, $version, $operator)) { - throw new \Exception("Version check failed! " . $package->getName() . " requires Roundcube version $operator $version, $rcubeVersion was detected."); - } - } + if (empty($extra['roundcube'])) { + return; + } + + foreach (array('min-version' => '>=', 'max-version' => '<=') as $key => $operator) { + if (empty($extra['roundcube'][$key])) { + continue; + } + + $version = $parser->normalize(str_replace('-git', '.999', $extra['roundcube'][$key])); + $constraint = new VersionConstraint($operator, $version); + if (!$constraint->versionCompare($rcubeVersion, $version, $operator)) { + throw new \Exception( + "Version check failed! " . $package->getName() + . " requires Roundcube version $operator $version, $rcubeVersion was detected." + ); } } } /** * Add or remove the given plugin to the list of active plugins in the Roundcube config. + * + * @param string $pluginName + * @param bool $add + * @return bool|int */ - private function rcubeAlterConfig($plugin_name, $add) + private function rcubeAlterConfig($pluginName, $add) { - $config_file = $this->rcubeConfigFile(); - @include($config_file); + $configFile = $this->rcubeConfigFile(); + @include($configFile); $success = false; $varname = '$config'; if (empty($config) && !empty($rcmail_config)) { - $config = $rcmail_config; + $config = $rcmail_config; $varname = '$rcmail_config'; } - if (is_array($config) && is_writeable($config_file)) { - $config_templ = @file_get_contents($config_file) ?: ''; - $config_plugins = !empty($config['plugins']) ? ((array) $config['plugins']) : array(); - $active_plugins = $config_plugins; + if (is_array($config) && is_writeable($configFile)) { + $configTemplate = @file_get_contents($configFile) ?: ''; + $configPlugins = !empty($config['plugins']) ? ((array)$config['plugins']) : array(); + $activePlugins = $configPlugins; - if ($add && !in_array($plugin_name, $active_plugins)) { - $active_plugins[] = $plugin_name; - } elseif (!$add && ($i = array_search($plugin_name, $active_plugins)) !== false) { - unset($active_plugins[$i]); + if ($add && !in_array($pluginName, $activePlugins)) { + $activePlugins[] = $pluginName; + } elseif (!$add && ($i = array_search($pluginName, $activePlugins)) !== false) { + unset($activePlugins[$i]); } - if ($active_plugins != $config_plugins) { - $count = 0; - $var_export = "array(\n\t'" . join("',\n\t'", $active_plugins) . "',\n);"; - $new_config = preg_replace( - "/(\\$varname\['plugins'\])\s+=\s+(.+);/Uims", - "\\1 = " . $var_export, - $config_templ, -1, $count); - - // 'plugins' option does not exist yet, add it... - if (!$count) { - $var_txt = "\n{$varname}['plugins'] = $var_export;\n"; - $new_config = str_replace('?>', $var_txt . '?>', $config_templ, $count); - + if ($activePlugins != $configPlugins) { + if ($activePlugins) { + $count = 0; + $varExport = "array(\n\t'" . join("',\n\t'", $activePlugins) . "',\n);"; + $newConfig = preg_replace( + "/(\\$varname\s*\[\s*['\"]plugins['\"]\s*\])\s*=\s*(.*);/Uis", "\\1 = $varExport", + $configTemplate, + -1, + $count + ); + + // 'plugins' option doesn't exist yet, add it... if (!$count) { - $new_config = $config_templ . $var_txt; + $varTxt = "\n// List of active plugins (in plugins/ directory)\n"; + $varTxt .= "{$varname}['plugins'] = $varExport\n"; + $newConfig = str_replace('?>', $varTxt . '?>', $configTemplate, $count); + + if (!$count) { + $newConfig = $configTemplate . $varTxt; + $count++; + } } + } else { + // No active plugins; empty plugin list + $newConfig = preg_replace( + "/(\\$varname\s*\[\s*['\"]plugins['\"]\s*\])\s*=\s*(.*);/Uis", "\\1 = array();", + $configTemplate, + -1, + $count + ); } - $success = file_put_contents($config_file, $new_config); + // Only write file if needed + $success = ($count) ? file_put_contents($configFile, $newConfig) : true; } } if ($success && php_sapi_name() == 'cli') { - $this->io->write("Updated local config at $config_file"); + $this->io->write("Updated local config at $configFile"); } return $success; @@ -243,6 +278,8 @@ private function rcubeAlterConfig($plugin_name, $add) /** * Helper method to get an absolute path to the local Roundcube config file + * + * @return bool|string */ private function rcubeConfigFile() { @@ -251,30 +288,36 @@ private function rcubeConfigFile() /** * Run the given script file + * + * @param $script + * @param PackageInterface $package + * @return void */ private function rcubeRunScript($script, PackageInterface $package) { - $plugin_name = $this->getPluginName($package); - $plugin_dir = $this->getVendorDir() . DIRECTORY_SEPARATOR . $plugin_name; + $pluginName = $this->getPluginName($package); + $pluginDir = $this->getVendorDir() . DIRECTORY_SEPARATOR . $pluginName; // check for executable shell script - if (($scriptfile = realpath($plugin_dir . DIRECTORY_SEPARATOR . $script)) && is_executable($scriptfile)) { - $script = $scriptfile; + if (($scriptFile = realpath($pluginDir . DIRECTORY_SEPARATOR . $script)) && is_executable($scriptFile)) { + $script = $scriptFile; } - // run PHP script in Roundcube context - if ($scriptfile && preg_match('/\.php$/', $scriptfile)) { + if ($scriptFile && preg_match('/\.php$/', $scriptFile)) { + // run PHP script in Roundcube context $incdir = realpath(getcwd() . '/program/include'); include_once($incdir . '/iniset.php'); - include($scriptfile); + include($scriptFile); + return; } + // attempt to execute the given string as shell commands - else { - $process = new ProcessExecutor($this->io); - $exitCode = $process->execute($script, null, $plugin_dir); - if ($exitCode !== 0) { - throw new \RuntimeException('Error executing script: '. $process->getErrorOutput(), $exitCode); - } + $process = new ProcessExecutor($this->io); + $exitCode = $process->execute($script, $output, $pluginDir); + unset($output); + + if ($exitCode !== 0) { + throw new \RuntimeException('Error executing script: ' . $process->getErrorOutput(), $exitCode); } } }