diff --git a/bin/setup b/bin/setup index e778de1f60..fa8296b1d4 100755 --- a/bin/setup +++ b/bin/setup @@ -24,6 +24,7 @@ use Alchemy\Phrasea\Command\Plugin\AddPlugin; use Alchemy\Phrasea\Command\Plugin\RemovePlugin; use Alchemy\Phrasea\Command\Plugin\EnablePlugin; use Alchemy\Phrasea\Command\Plugin\DisablePlugin; +use Alchemy\Phrasea\Command\Plugin\DownloadPlugin; use Alchemy\Phrasea\CLI; use Alchemy\Phrasea\Command\Setup\CheckEnvironment; use Alchemy\Phrasea\Core\CLIProvider\DoctrineMigrationServiceProvider; @@ -50,7 +51,7 @@ $app = new CLI(" This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions; type `about:license' for details.\n\n" - . ' SETUP', $version->getName() . ' ' . $version->getNumber()); + . ' SETUP', $version->getName() . ' ' . $version->getNumber()); $app->register(new DoctrineMigrationServiceProvider()); @@ -70,6 +71,7 @@ if ($configurationTester->isInstalled()) { } $app->command(new AddPlugin()); +$app->command(new DownloadPlugin()); $app->command(new ListPlugin()); $app->command(new RemovePlugin()); $app->command(new PluginsReset()); diff --git a/composer.json b/composer.json index cb2c9a9c74..3ef388b370 100644 --- a/composer.json +++ b/composer.json @@ -120,7 +120,8 @@ "google/recaptcha": "^1.1", "facebook/graph-sdk": "^5.6", "box/spout": "^2.7", - "paragonie/random-lib": "^2.0" + "paragonie/random-lib": "^2.0", + "czproject/git-php": "^3.17" }, "require-dev": { "mikey179/vfsstream": "~1.5", diff --git a/composer.lock b/composer.lock index c379c82566..6a46737c3e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a9c7fed873d5bfe962c81d543b33330f", + "content-hash": "64830cb4d53b32b47e02d4a19df9cef2", "packages": [ { "name": "alchemy-fr/tcpdf-clone", @@ -1156,6 +1156,48 @@ ], "time": "2016-08-09T20:10:17+00:00" }, + { + "name": "czproject/git-php", + "version": "v3.17.0", + "source": { + "type": "git", + "url": "https://github.com/czproject/git-php.git", + "reference": "a7b911b81a2fe626f748a4ac8955353c5777bc6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/czproject/git-php/zipball/a7b911b81a2fe626f748a4ac8955353c5777bc6c", + "reference": "a7b911b81a2fe626f748a4ac8955353c5777bc6c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "nette/tester": "^1.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jan Pecha", + "email": "janpecha@email.cz" + } + ], + "description": "Library for work with Git repository in PHP.", + "keywords": [ + "git" + ], + "time": "2019-02-09T13:11:36+00:00" + }, { "name": "dailymotion/sdk", "version": "1.6.5", @@ -7747,12 +7789,12 @@ "version": "v1.6.4", "source": { "type": "git", - "url": "https://github.com/bovigo/vfsStream.git", + "url": "https://github.com/mikey179/vfsStream.git", "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/0247f57b2245e8ad2e689d7cee754b45fbabd592", + "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/0247f57b2245e8ad2e689d7cee754b45fbabd592", "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592", "shasum": "" }, diff --git a/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php b/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php index b2c72999d1..2d9d211612 100644 --- a/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php +++ b/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php @@ -51,4 +51,41 @@ abstract class AbstractPluginCommand extends Command $this->container['plugins.autoloader-generator']->write($manifests); $output->writeln(" OK"); } + + protected function doInstallPlugin($source, InputInterface $input, OutputInterface $output) + { + $temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory(); + + $output->write("Importing $source..."); + $this->container['plugins.importer']->import($source, $temporaryDir); + $output->writeln(" OK"); + + $output->write("Validating plugin..."); + $manifest = $this->container['plugins.plugins-validator']->validatePlugin($temporaryDir); + $output->writeln(" OK found ".$manifest->getName().""); + + $targetDir = $this->container['plugin.path'] . DIRECTORY_SEPARATOR . $manifest->getName(); + + $output->write("Setting up composer..."); + $this->container['plugins.composer-installer']->install($temporaryDir); + $output->writeln(" OK"); + + $output->write("Installing plugin ".$manifest->getName()."..."); + $this->container['filesystem']->mirror($temporaryDir, $targetDir); + $output->writeln(" OK"); + + $output->write("Copying public files ".$manifest->getName()."..."); + $this->container['plugins.assets-manager']->update($manifest); + $output->writeln(" OK"); + + $output->write("Removing temporary directory..."); + $this->container['filesystem']->remove($temporaryDir); + $output->writeln(" OK"); + + $output->write("Activating plugin..."); + $this->container['conf']->set(['plugins', $manifest->getName(), 'enabled'], true); + $output->writeln(" OK"); + + $this->updateConfigFiles($input, $output); + } } diff --git a/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php b/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php index 36db372da2..0cba6d047e 100644 --- a/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php +++ b/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php @@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Command\Plugin; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\ArrayInput; class AddPlugin extends AbstractPluginCommand { @@ -29,41 +30,36 @@ class AddPlugin extends AbstractPluginCommand protected function doExecutePluginAction(InputInterface $input, OutputInterface $output) { $source = $input->getArgument('source'); + $shouldDownload = $this->shouldDownloadPlugin($source); - $temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory(); + if ($shouldDownload){ + $command = $this->getApplication()->find('plugins:download'); + $arguments = [ + 'command' => 'plugins:download', + 'source' => $source, + 'shouldInstallPlugin' => true + ]; - $output->write("Importing $source..."); - $this->container['plugins.importer']->import($source, $temporaryDir); - $output->writeln(" OK"); + $downloadInput = new ArrayInput($arguments); + $command->run($downloadInput, $output); - $output->write("Validating plugin..."); - $manifest = $this->container['plugins.plugins-validator']->validatePlugin($temporaryDir); - $output->writeln(" OK found ".$manifest->getName().""); + } else { - $targetDir = $this->container['plugin.path'] . DIRECTORY_SEPARATOR . $manifest->getName(); - - $output->write("Setting up composer..."); - $this->container['plugins.composer-installer']->install($temporaryDir); - $output->writeln(" OK"); - - $output->write("Installing plugin ".$manifest->getName()."..."); - $this->container['filesystem']->mirror($temporaryDir, $targetDir); - $output->writeln(" OK"); - - $output->write("Copying public files ".$manifest->getName()."..."); - $this->container['plugins.assets-manager']->update($manifest); - $output->writeln(" OK"); - - $output->write("Removing temporary directory..."); - $this->container['filesystem']->remove($temporaryDir); - $output->writeln(" OK"); - - $output->write("Activating plugin..."); - $this->container['conf']->set(['plugins', $manifest->getName(), 'enabled'], true); - $output->writeln(" OK"); - - $this->updateConfigFiles($input, $output); + $this->doInstallPlugin($source, $input, $output); + } return 0; } + + protected function shouldDownloadPlugin($source) + { + $allowedScheme = array('https','ssh'); + + $scheme = parse_url($source, PHP_URL_SCHEME); + if (in_array($scheme, $allowedScheme)){ + return true; + } else{ + return false; + } + } } diff --git a/lib/Alchemy/Phrasea/Command/Plugin/DownloadPlugin.php b/lib/Alchemy/Phrasea/Command/Plugin/DownloadPlugin.php new file mode 100644 index 0000000000..a3fdacbc1c --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/Plugin/DownloadPlugin.php @@ -0,0 +1,157 @@ +setDescription('Downloads a plugin to Phraseanet') + ->addArgument('source', InputArgument::REQUIRED, 'The source is a remote url (.zip or .git)') + ->addArgument('destination', InputArgument::OPTIONAL, 'Download destination') + ->addArgument('shouldInstallPlugin', InputArgument::OPTIONAL, 'True or false, determines if plugin should be installed after download'); + } + + protected function doExecutePluginAction(InputInterface $input, OutputInterface $output) + { + $source = $input->getArgument('source'); + $destination = $input->getArgument('destination'); + $shouldInstallPlugin = false; + $shouldInstallPlugin = $input->getArgument('shouldInstallPlugin'); + + $destinationSubdir = '/plugin-'.md5($source); + + if ($destination){ + + $destination = trim($destination); + $destination = rtrim($destination, '/'); + + $localDownloadPath = $destination; + + } else { + + $localDownloadPath = '/tmp/plugin-download' . $destinationSubdir; + } + + if (!is_dir($localDownloadPath)) { + mkdir($localDownloadPath, 0755, true); + } + + $extension = $this->getURIExtension($source); + + if ($extension){ + + switch ($extension){ + + case 'zip': + + $localUnpackPath = '/tmp/plugin-zip'. $destinationSubdir; + + if (!is_dir($localUnpackPath)) { + mkdir($localUnpackPath, 0755, true); + } + + $localArchiveFile = $localUnpackPath . '/plugin-downloaded.zip'; + + // download + $output->writeln("Downloading $source..."); + set_time_limit(0); + $fp = fopen ($localArchiveFile, 'w+'); + $ch = curl_init($source);; + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_exec($ch); + curl_close($ch); + fclose($fp); + + // unpack + $output->writeln("Unpacking $source..."); + $zip = new \ZipArchive(); + $errorUnpack = false; + + if ($zip->open($localArchiveFile)) { + for ($i = 0; $i < $zip->numFiles; $i++) { + if (!($zip->extractTo($localDownloadPath, array($zip->getNameIndex($i))))) { + $errorUnpack = true; + } + } + $zip->close(); + } + + if ($errorUnpack){ + $output->writeln("Failed unzipping $source"); + } else { + $output->writeln("Plugin downloaded to $localDownloadPath"); + if ($shouldInstallPlugin) $this->doInstallPlugin($localDownloadPath, $input, $output); + } + + // remove zip archive + $this->delDirTree($localUnpackPath); + + break; + + case 'git': + $output->writeln("Downloading $source..."); + $repo = GitRepository::cloneRepository($source, $localDownloadPath); + $output->writeln("Plugin downloaded to $localDownloadPath"); + if ($shouldInstallPlugin) $this->doInstallPlugin($localDownloadPath, $input, $output); + break; + + } + + } else { + + $output->writeln("The source $source is not supported. Only .zip and .git are supported."); + } + + return 0; + } + + protected function getURIExtension($source) + { + $validExtension = false; + $allowedExtension = array('zip','git'); + + $path = parse_url($source, PHP_URL_PATH); + if (strpos($path, '.') !== false) { + $pathParts = explode('.', $path); + $extension = $pathParts[1]; + if (in_array($extension, $allowedExtension)){ + $validExtension = true; + } + } + + if ($validExtension){ + return $extension; + } else { + return false; + } + } + + protected static function delDirTree($dir) { + $files = array_diff(scandir($dir), array('.','..')); + foreach ($files as $file) { + (is_dir("$dir/$file")) ? self::delDirTree("$dir/$file") : unlink("$dir/$file"); + } + return rmdir($dir); + } +}