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);
+ }
+}