Merge pull request #368 from romainneutron/plugins

[3.8] Welcome Plugins
This commit is contained in:
Romain Neutron
2013-05-31 07:38:46 -07:00
86 changed files with 3091 additions and 38 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@
/config/connexions.yml
.DS_Store
/vendor
/plugins
composer.phar
behat.yml
/datas

View File

@@ -27,6 +27,7 @@
- Add bin/developer console for developement purpose.
- Add possibility to delete a basket from the workzone basket browser.
- Add localized labels for databox documentary fields.
- Add plugin architecture for third party modules and customization.
* 3.7.12 (2013-05-13)

View File

@@ -24,8 +24,10 @@ use Alchemy\Phrasea\Command\RecordAdd;
use Alchemy\Phrasea\Command\RescanTechnicalDatas;
use Alchemy\Phrasea\Command\UpgradeDBDatas;
use Alchemy\Phrasea\CLI;
use Alchemy\Phrasea\Command\Plugin\AddPlugin;
use Alchemy\Phrasea\Command\Plugin\RemovePlugin;
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/autoload.php';
try {
$app = new CLI("
@@ -90,6 +92,9 @@ try {
$app->command(new RescanTechnicalDatas('records:rescan-technical-datas'));
$app->command(new BuildMissingSubdefs('records:build-missing-subdefs'));
$app->command(new AddPlugin());
$app->command(new RemovePlugin());
$result_code = is_int($app->run()) ? : 1;
} catch (\Exception $e) {
$result_code = 1;

View File

@@ -36,7 +36,7 @@ use Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand;
use Doctrine\ORM\Tools\Console\Command\RunDqlCommand;
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand;
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/autoload.php';
try {
$cli = new CLI("

View File

@@ -16,8 +16,10 @@
"facebook/php-sdk" : "~3.0",
"gedmo/doctrine-extensions" : "~2.3.0",
"google-plus/api-client" : "~0.6.1",
"guzzle/guzzle" : "~3.0",
"imagine/imagine" : "dev-ColorSpaces@dev",
"jms/serializer" : "~0.10",
"justinrainbow/json-schema" : "~1.3",
"mediavorus/mediavorus" : "~0.3",
"media-alchemyst/media-alchemyst" : "~0.2.3",
"monolog/monolog" : "~1.3.0",
@@ -27,6 +29,7 @@
"neutron/silex-filesystem-provider": "dev-master",
"neutron/sphinxsearch-api" : "~2.0.6",
"neutron/recaptcha" : "~0.1.0",
"neutron/temporary-filesystem" : "~2.0",
"php-xpdf/php-xpdf" : "~0.1.1",
"phpexiftool/phpexiftool" : "~0.2, >=0.2.2",
"silex/silex" : "~1.0.0",

158
composer.lock generated
View File

@@ -3,7 +3,7 @@
"This file locks the dependencies of your project to a known state",
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
],
"hash": "7fd45681e93a7fe1d0f4e691818ad4e2",
"hash": "06eddf18808e8091de82ec3b5d6b699e",
"packages": [
{
"name": "BadFaith/BadFaith",
@@ -1091,6 +1091,64 @@
],
"time": "2013-03-28 16:41:24"
},
{
"name": "justinrainbow/json-schema",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "1.3.1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/1.3.1",
"reference": "1.3.1",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"bin": [
"bin/validate-json"
],
"type": "library",
"autoload": {
"psr-0": {
"JsonSchema": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch",
"homepage": "http://wiedler.ch/igor/"
},
{
"name": "Bruno Prieto Reis",
"email": "bruno.p.reis@gmail.com"
},
{
"name": "Justin Rainbow",
"email": "justin.rainbow@gmail.com"
},
{
"name": "Robert Schönthal",
"email": "seroscho@googlemail.com",
"homepage": "http://digitalkaoz.net"
}
],
"description": "A library to validate a json schema.",
"homepage": "https://github.com/justinrainbow/json-schema",
"keywords": [
"json",
"schema"
],
"time": "2013-02-21 04:49:21"
},
{
"name": "media-alchemyst/media-alchemyst",
"version": "0.2.5",
@@ -1540,6 +1598,43 @@
],
"time": "2012-10-23 02:05:12"
},
{
"name": "neutron/temporary-filesystem",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/romainneutron/Temporary-Filesystem.git",
"reference": "2.0.1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/romainneutron/Temporary-Filesystem/zipball/2.0.1",
"reference": "2.0.1",
"shasum": ""
},
"require": {
"symfony/filesystem": ">=2.0,<3.0"
},
"type": "library",
"autoload": {
"psr-0": {
"Neutron": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Romain Neutron",
"email": "imprec@gmail.com",
"homepage": "http://www.lickmychip.com/"
}
],
"description": "Symfony filesystem extension to handle temporary files",
"time": "2013-04-09 08:38:21"
},
{
"name": "php-ffmpeg/php-ffmpeg",
"version": "0.2.4",
@@ -2722,21 +2817,21 @@
},
{
"name": "behat/mink",
"version": "v1.4.3",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/Mink.git",
"reference": "v1.4.3"
"reference": "v1.5.0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Behat/Mink/zipball/v1.4.3",
"reference": "v1.4.3",
"url": "https://api.github.com/repos/Behat/Mink/zipball/v1.5.0",
"reference": "v1.5.0",
"shasum": ""
},
"require": {
"php": ">=5.3.1",
"symfony/css-selector": ">=2.0,<2.4-dev"
"symfony/css-selector": ">=2.0,<3.0"
},
"suggest": {
"behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
@@ -2747,7 +2842,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-develop": "1.4.x-dev"
"dev-develop": "1.5.x-dev"
}
},
"autoload": {
@@ -2773,24 +2868,24 @@
"testing",
"web"
],
"time": "2013-03-02 15:53:18"
"time": "2013-04-13 23:39:27"
},
{
"name": "behat/mink-browserkit-driver",
"version": "v1.0.5",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/MinkBrowserKitDriver.git",
"reference": "v1.0.5"
"reference": "v1.1.0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Behat/MinkBrowserKitDriver/zipball/v1.0.5",
"reference": "v1.0.5",
"url": "https://api.github.com/repos/Behat/MinkBrowserKitDriver/zipball/v1.1.0",
"reference": "v1.1.0",
"shasum": ""
},
"require": {
"behat/mink": ">=1.4.3,<1.5",
"behat/mink": ">=1.5,<1.6",
"php": ">=5.3.1",
"symfony/browser-kit": ">=2.0,<3.0",
"symfony/dom-crawler": ">=2.0,<3.0"
@@ -2801,7 +2896,7 @@
"type": "mink-driver",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "1.1.x-dev"
}
},
"autoload": {
@@ -2828,30 +2923,29 @@
"browser",
"testing"
],
"time": "2013-04-13 12:17:15"
"time": "2013-04-13 23:46:30"
},
{
"name": "behat/mink-extension",
"version": "v1.0.1",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/MinkExtension.git",
"reference": "v1.0.1"
"reference": "v1.1.0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Behat/MinkExtension/zipball/v1.0.1",
"reference": "v1.0.1",
"url": "https://api.github.com/repos/Behat/MinkExtension/zipball/v1.1.0",
"reference": "v1.1.0",
"shasum": ""
},
"require": {
"behat/behat": ">=2.4,<2.5-dev",
"behat/mink": ">=1.4,<1.5-dev",
"behat/behat": ">=2.4.5,<2.5",
"behat/mink": ">=1.4.3,<1.6-dev",
"php": ">=5.3.2"
},
"require-dev": {
"behat/mink-goutte-driver": "1.0.*",
"fabpot/goutte": "1.0.*@dev"
"behat/mink-goutte-driver": ">=1.0,<2.0"
},
"type": "behat-extension",
"extra": {
@@ -2883,7 +2977,7 @@
"test",
"web"
],
"time": "2013-02-19 23:49:16"
"time": "2013-05-23 12:32:57"
},
{
"name": "behat/mink-goutte-driver",
@@ -2938,27 +3032,27 @@
},
{
"name": "behat/mink-selenium2-driver",
"version": "v1.0.6",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/Behat/MinkSelenium2Driver.git",
"reference": "v1.0.6"
"reference": "v1.1.0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Behat/MinkSelenium2Driver/zipball/v1.0.6",
"reference": "v1.0.6",
"url": "https://api.github.com/repos/Behat/MinkSelenium2Driver/zipball/v1.1.0",
"reference": "v1.1.0",
"shasum": ""
},
"require": {
"behat/mink": ">=1.4.3,<1.5",
"behat/mink": ">=1.5,<1.6",
"instaclick/php-webdriver": ">=1.0.12.0,<1.1",
"php": ">=5.3.1"
},
"type": "mink-driver",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "1.1.x-dev"
}
},
"autoload": {
@@ -2992,7 +3086,7 @@
"testing",
"webdriver"
],
"time": "2013-04-13 12:56:28"
"time": "2013-04-13 23:51:01"
},
{
"name": "doctrine/data-fixtures",
@@ -3037,7 +3131,7 @@
],
"authors": [
{
"name": "Jonathan H. Wage",
"name": "Jonathan Wage",
"email": "jonwage@gmail.com",
"homepage": "http://www.jwage.com/"
}

View File

@@ -89,9 +89,11 @@ use Alchemy\Phrasea\Core\Provider\NotificationDelivererServiceProvider;
use Alchemy\Phrasea\Core\Provider\ORMServiceProvider;
use Alchemy\Phrasea\Core\Provider\PhraseanetServiceProvider;
use Alchemy\Phrasea\Core\Provider\PhraseaVersionServiceProvider;
use Alchemy\Phrasea\Core\Provider\PluginServiceProvider;
use Alchemy\Phrasea\Core\Provider\RegistrationServiceProvider;
use Alchemy\Phrasea\Core\Provider\SearchEngineServiceProvider;
use Alchemy\Phrasea\Core\Provider\TaskManagerServiceProvider;
use Alchemy\Phrasea\Core\Provider\TemporaryFilesystemServiceProvider;
use Alchemy\Phrasea\Core\Provider\TokensServiceProvider;
use Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
@@ -241,6 +243,7 @@ class Application extends SilexApplication
$this->register(new InstallerServiceProvider());
$this->register(new PhraseanetServiceProvider());
$this->register(new PhraseaVersionServiceProvider());
$this->register(new PluginServiceProvider());
$this->register(new PHPExiftoolServiceProvider());
$this->register(new ReCaptchaServiceProvider());
@@ -262,6 +265,7 @@ class Application extends SilexApplication
$this->register(new ServiceControllerServiceProvider());
$this->register(new SwiftmailerServiceProvider());
$this->register(new TaskManagerServiceProvider());
$this->register(new TemporaryFilesystemServiceProvider());
$this->register(new TokensServiceProvider());
$this->register(new TwigServiceProvider(), array(
'twig.options' => array(
@@ -397,6 +401,10 @@ class Application extends SilexApplication
return $data[1];
};
call_user_func(function ($app) {
require $app['plugins.directory'] . '/services.php';
}, $this);
}
/**

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Plugin;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
abstract class AbstractPluginCommand extends Command
{
protected function validatePlugins(InputInterface $input, OutputInterface $output)
{
$manifests = array();
$output->write("Validating plugins...");
foreach ($this->container['plugins.explorer'] as $directory) {
$manifests[] = $manifest = $this->container['plugins.plugins-validator']->validatePlugin($directory);
}
$output->writeln(" <comment>OK</comment>");
return $manifests;
}
protected function updateConfigFiles(InputInterface $input, OutputInterface $output)
{
$manifests = $this->validatePlugins($input, $output);
$output->write("Updating config files...");
$this->container['plugins.autoloader-generator']->write($manifests);
$output->writeln(" <comment>OK</comment>");
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Plugin;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
class AddPlugin extends AbstractPluginCommand
{
public function __construct()
{
parent::__construct('plugins:add');
$this
->setDescription('Installs a plugin to Phraseanet')
->addArgument('source', InputArgument::REQUIRED, 'The source is a folder');
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$source = $input->getArgument('source');
$temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory();
$output->write("Importing <info>$source</info>...");
$this->container['plugins.importer']->import($source, $temporaryDir);
$output->writeln(" <comment>OK</comment>");
$output->write("Validating plugin...");
$manifest = $this->container['plugins.plugins-validator']->validatePlugin($temporaryDir);
$output->writeln(" <comment>OK</comment> found <info>".$manifest->getName()."</info>");
$targetDir = $this->container['plugins.directory'] . DIRECTORY_SEPARATOR . $manifest->getName();
$output->write("Setting up composer...");
$this->container['plugins.composer-installer']->install($temporaryDir);
$output->writeln(" <comment>OK</comment>");
$output->write("Installing plugin <info>".$manifest->getName()."</info>...");
$this->container['filesystem']->mirror($temporaryDir, $targetDir);
$output->writeln(" <comment>OK</comment>");
$output->write("Removing temporary directory...");
$this->container['filesystem']->remove($temporaryDir);
$output->writeln(" <comment>OK</comment>");
$this->updateConfigFiles($input, $output);
return 0;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Plugin;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
class RemovePlugin extends AbstractPluginCommand
{
public function __construct()
{
parent::__construct('plugins:remove');
$this
->setDescription('Removes a plugin given its name')
->addArgument('name', InputArgument::REQUIRED, 'The name of the plugin');
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$name = $input->getArgument('name');
$path = $this->container['plugins.directory'] . DIRECTORY_SEPARATOR . $name;
$output->write("Removing <info>$name</info>...");
$this->container['filesystem']->remove($path);
$output->writeln(" <comment>OK</comment>");
$this->updateConfigFiles($input, $output);
return 0;
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Provider;
use Alchemy\Phrasea\Plugin\Schema\ManifestValidator;
use Alchemy\Phrasea\Plugin\Management\PluginsExplorer;
use Alchemy\Phrasea\Plugin\Management\ComposerInstaller;
use Alchemy\Phrasea\Plugin\Schema\PluginValidator;
use Alchemy\Phrasea\Plugin\Importer\Importer;
use Alchemy\Phrasea\Plugin\Importer\ImportStrategy;
use Alchemy\Phrasea\Plugin\Importer\FolderImporter;
use Alchemy\Phrasea\Plugin\Management\AutoloaderGenerator;
use Guzzle\Http\Client as Guzzle;
use JsonSchema\Validator as JsonValidator;
use Symfony\Component\Process\ExecutableFinder;
use Silex\Application;
use Silex\ServiceProviderInterface;
class PluginServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['plugins.directory'] = realpath(__DIR__ . '/../../../../../plugins');
$app['plugins.schema'] = realpath(__DIR__ . '/../../../../conf.d/plugin-schema.json');
$app['plugins.json-validator'] = $app->share(function (Application $app) {
return new JsonValidator();
});
$app['plugins.manifest-validator'] = $app->share(function (Application $app) {
return ManifestValidator::create($app);
});
$app['plugins.plugins-validator'] = $app->share(function (Application $app) {
return new PluginValidator($app['plugins.manifest-validator']);
});
$app['plugins.import-strategy'] = $app->share(function (Application $app) {
return new ImportStrategy();
});
$app['plugins.autoloader-generator'] = $app->share(function (Application $app) {
return new AutoloaderGenerator($app['plugins.directory']);
});
$app['plugins.guzzle'] = $app->share(function (Application $app) {
return new Guzzle();
});
$app['plugins.composer-installer'] = $app->share(function (Application $app) {
$phpBinary = $app['phraseanet.registry']->get('php_binary');
if (!is_executable($phpBinary)) {
$finder = new ExecutableFinder();
$phpBinary = $finder->find('php');
}
return new ComposerInstaller($app['plugins.directory'], $app['plugins.guzzle'], $phpBinary);
});
$app['plugins.explorer'] = $app->share(function (Application $app) {
return new PluginsExplorer($app['plugins.directory']);
});
$app['plugins.importer'] = $app->share(function (Application $app) {
return new Importer($app['plugins.import-strategy'], array(
'plugins.importer.folder-importer' => $app['plugins.importer.folder-importer'],
));
});
$app['plugins.importer.folder-importer'] = $app->share(function (Application $app) {
return new FolderImporter($app['filesystem']);
});
}
public function boot(Application $app)
{
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Provider;
use Neutron\TemporaryFilesystem\TemporaryFilesystem;
use Silex\Application;
use Silex\ServiceProviderInterface;
class TemporaryFilesystemServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['temporary-filesystem'] = $app->share(function (Application $app) {
return new TemporaryFilesystem($app['filesystem']);
});
}
public function boot(Application $app)
{
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Exception;
use Alchemy\Phrasea\Exception\RuntimeException;
class ComposerInstallException extends RuntimeException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Exception;
use Alchemy\Phrasea\Exception\RuntimeException;
class ImportFailureException extends RuntimeException
{
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Exception;
use Alchemy\Phrasea\Exception\RuntimeException;
class JsonValidationException extends RuntimeException
{
protected $errors;
public function __construct($message, $errors = array())
{
$this->errors = $errors;
parent::__construct($message);
}
public function getErrors()
{
return $this->errors;
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Exception;
use Alchemy\Phrasea\Exception\RuntimeException;
class PluginValidationException extends RuntimeException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Exception;
use Alchemy\Phrasea\Exception\RuntimeException;
class RegistrationFailureException extends RuntimeException
{
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Importer;
use Alchemy\Phrasea\Plugin\Exception\ImportFailureException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\ExceptionInterface as FsException;
class FolderImporter implements ImporterInterface
{
private $fs;
public function __construct(Filesystem $fs)
{
$this->fs = $fs;
}
/**
* {@inheritdoc}
*/
public function import($source, $target)
{
try {
$this->fs->mirror($source, $target);
} catch (FsException $e) {
try {
$this->fs->remove($target);
} catch (FsException $e) {
}
throw new ImportFailureException(sprintf('Unable to import from `%s` to `%s`', $source, $target), $e->getCode(), $e);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Importer;
use Alchemy\Phrasea\Plugin\Exception\ImportFailureException;
class ImportStrategy
{
public function detect($source)
{
switch (true) {
case file_exists($source) && is_dir($source):
return 'plugins.importer.folder-importer';
default:
throw new ImportFailureException(sprintf('Unable to detect source type for `%s`', $source));
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Importer;
use Alchemy\Phrasea\Plugin\Exception\ImportFailureException;
class Importer
{
private $importers;
private $strategy;
public function __construct(ImportStrategy $strategy, $importers)
{
$this->importers = $importers;
$this->strategy = $strategy;
}
/**
*
* @param string $source
* @param string $target
*
* @throws ImportFailureException
*/
public function import($source, $target)
{
$strategy = $this->strategy->detect($source);
if (!isset($this->importers[$strategy])) {
throw new ImportFailureException(sprintf('Unable to get an import for source `%s`', $source));
}
$this->importers[$strategy]->import($source, $target);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Importer;
use Alchemy\Phrasea\Plugin\Exception\ImportFailureException;
interface ImporterInterface
{
/**
* @param $source
* @param $target
*
* @throws ImportFailureException In case the import failed
*/
public function import($source, $target);
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Management;
use Alchemy\Phrasea\Plugin\Exception\RegistrationFailureException;
class AutoloaderGenerator
{
private $pluginDirectory;
public function __construct($pluginDirectory)
{
$this->pluginDirectory = $pluginDirectory;
}
public function write($manifests)
{
$this
->doWrite('autoload.php', $this->createLoader($manifests))
->doWrite('services.php', $this->createServices($manifests));
return $this;
}
private function doWrite($file, $data)
{
if (false === file_put_contents($this->pluginDirectory . DIRECTORY_SEPARATOR . $file, $data)) {
throw new RegistrationFailureException(sprintf('Failed to write %s', $file));
}
return $this;
}
private function createLoader($manifests)
{
$buffer = <<<EOF
<?php
// This file is automatically generated, please do not edit it.
// To update configuration, use bin/console plugins:* commands.
return call_user_func(function () {
EOF;
foreach ($manifests as $manifest) {
$autoloader = '/' . $manifest->getName() . DIRECTORY_SEPARATOR . "vendor" . DIRECTORY_SEPARATOR . "autoload.php";
$buffer .= <<<EOF
require __DIR__ . '$autoloader';
EOF;
}
// composer loader are preprent
$autoloader = '/../vendor/autoload.php';
$buffer .= <<<EOF
\$loader = require __DIR__ . '$autoloader';
return \$loader;\n});
EOF;
return $buffer;
}
private function createServices($manifests)
{
$buffer = <<<EOF
<?php
// This file is automatically generated, please do not edit it.
// To update configuration, use bin/console plugins:* commands.
use Alchemy\Phrasea\Application;
return call_user_func(function (Application \$app) {
EOF;
foreach ($manifests as $manifest) {
foreach ($manifest->getServices() as $service) {
$class = $service['class'];
$buffer .= <<<EOF
\$app->register($class::create(\$app));
EOF;
}
}
$buffer .= <<<EOF
return \$app;
}, \$app);
EOF;
return $buffer;
}
}

View File

@@ -0,0 +1,124 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Management;
use Alchemy\Phrasea\Plugin\Exception\ComposerInstallException;
use Symfony\Component\Process\ProcessBuilder;
use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;
use Guzzle\Common\Exception\GuzzleException;
use Guzzle\Http\Client as Guzzle;
class ComposerInstaller
{
private $composer;
private $guzzle;
private $pluginsDirectory;
private $phpExecutable;
public function __construct($pluginsDirectory, Guzzle $guzzle, $phpExecutable)
{
if (!is_executable($phpExecutable)) {
throw new ComposerInstallException(sprintf('`%s` is not a valid PHP executable', $phpExecutable));
}
$this->guzzle = $guzzle;
$this->pluginsDirectory = $pluginsDirectory;
$this->phpExecutable = $phpExecutable;
$this->composer = $this->pluginsDirectory . DIRECTORY_SEPARATOR . 'composer.phar';
}
public function install($directory)
{
$process = $this->createProcessBuilder()
->add('install')
->add('--working-dir')
->add($directory)
->add('--no-dev')
->add('--optimize-autoloader')
->getProcess();
try {
$process->run();
} catch (ProcessException $e) {
throw new ComposerInstallException(sprintf('Unable to composer install %s', $directory), $e->getCode(), $e);
}
if (!$process->isSuccessful()) {
throw new ComposerInstallException(sprintf('Unable to composer install %s', $directory));
}
}
/**
* @return ProcessBuilder
*/
private function createProcessBuilder()
{
if (!file_exists($this->composer)) {
$this->installComposer();
} else {
$process = ProcessBuilder::create(array(
$this->phpExecutable, $this->composer, 'self-update'
))->getProcess();
$process->run();
}
return ProcessBuilder::create(array($this->phpExecutable, $this->composer));
}
private function installComposer()
{
$installer = $this->pluginsDirectory . DIRECTORY_SEPARATOR . 'installer';
$handle = fopen($installer, 'w+');
$request = $this->guzzle->get('https://getcomposer.org/installer', null, $handle);
try {
$response = $request->send();
fclose($handle);
} catch (GuzzleException $e) {
fclose($handle);
throw new ComposerInstallException('Unable to download composer install script.');
}
if (200 !== $response->getStatusCode()) {
@unlink($installer);
throw new ComposerInstallException('Unable to download composer install script.');
}
$dir = getcwd();
if (!@chdir($this->pluginsDirectory)) {
throw new ComposerInstallException('Unable to move to plugins directory for composer install.');
}
$process = ProcessBuilder::create(array($this->phpExecutable, $installer))->getProcess();
try {
$process->run();
@unlink($installer);
} catch (ProcessException $e) {
@unlink($installer);
throw new ComposerInstallException('Unable run composer install script.');
}
if (!@chdir($dir)) {
throw new ComposerInstallException('Unable to move to plugins directory for composer install.');
}
if (!$process->isSuccessful()) {
throw new ComposerInstallException('Composer install failed.');
}
if (!file_exists($this->composer)) {
throw new ComposerInstallException('Composer install failed.');
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Management;
use Symfony\Component\Finder\Finder;
class PluginsExplorer implements \IteratorAggregate, \Countable
{
private $pluginsDirectory;
public function __construct($pluginsDirectory)
{
$this->pluginsDirectory = $pluginsDirectory;
}
public function getIterator()
{
return $this->getFinder()->getIterator();
}
public function count()
{
return $this->getFinder()->count();
}
private function getFinder()
{
$finder = Finder::create();
return $finder
->ignoreDotFiles(true)
->ignoreVCS(true)
->useBestAdapter()
->directories()
->in($this->pluginsDirectory)
->depth(0);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin;
use Alchemy\Phrasea\Application;
use Silex\ServiceProviderInterface;
interface PluginProviderInterface extends ServiceProviderInterface
{
/**
* Factory for the plugin.
*
* This method is called to build it.
*
* @param Application $app
*
* @return PluginProviderInterface
*/
public static function create(Application $app);
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Schema;
class Manifest
{
private $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function getName()
{
return $this->get('name');
}
public function getDescription()
{
return $this->get('description');
}
public function getKeywords()
{
return $this->get('keywords');
}
public function getAuthors()
{
return $this->get('authors');
}
public function getHomepage()
{
return $this->get('homepage');
}
public function getLicense()
{
return $this->get('license');
}
public function getVersion()
{
return $this->get('version');
}
public function getMinimumPhraseanetVersion()
{
return $this->get('minimum-phraseanet-version');
}
public function getMaximumPhraseanetVersion()
{
return $this->get('maximum-phraseanet-version');
}
public function getServices()
{
return $this->get('services');
}
public function getExtra()
{
return $this->get('extra');
}
private function get($key)
{
return isset($this->data[$key]) ? $this->data[$key] : null;
}
}

View File

@@ -0,0 +1,89 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Schema;
use Alchemy\Phrasea\Application;
use JsonSchema\Validator as JsonValidator;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Alchemy\Phrasea\Plugin\Exception\JsonValidationException;
use Alchemy\Phrasea\Core\Version;
class ManifestValidator
{
private $validator;
private $version;
private $schemaData;
public function __construct(JsonValidator $validator, $schemaData, Version $version)
{
if (!is_object($schemaData)) {
throw new InvalidArgumentException('Json Schema must be an object');
}
$this->validator = $validator;
$this->version = $version;
$this->schemaData = $schemaData;
}
public function validate($data)
{
if (!is_object($data)) {
throw new InvalidArgumentException('Json Schema must be an object');
}
$this->validator->reset();
$this->validator->check($data, $this->schemaData);
if (!$this->validator->isValid()) {
$errors = array();
foreach ((array) $this->validator->getErrors() as $error) {
$errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message'];
}
throw new JsonValidationException('Manifest file does not match the expected JSON schema', $errors);
}
if (!preg_match('/^[a-z0-9-_]+$/i', $data->name)) {
throw new JsonValidationException('Does not match the expected JSON schema', array('"name" must not contains only alphanumeric caracters'));
}
if (isset($data->{'minimum-phraseanet-version'})) {
if (true !== version_compare($this->version->getNumber(), $data->{'minimum-phraseanet-version'}, '>=')) {
throw new JsonValidationException(sprintf(
'Version incomptibility : Minimum Phraseanet version required is %s, current version is %s',
$data->{'minimum-phraseanet-version'},
$this->version->getNumber()
));
}
}
if (isset($data->{'maximum-phraseanet-version'})) {
if (true !== version_compare($this->version->getNumber(), $data->{'maximum-phraseanet-version'}, '<')) {
throw new JsonValidationException(sprintf(
'Version incomptibility : Maximum Phraseanet version required is %s, current version is %s',
$data->{'maximum-phraseanet-version'},
$this->version->getNumber()
));
}
}
}
public static function create(Application $app)
{
$data = @json_decode(@file_get_contents($app['plugins.schema']));
if (JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidArgumentException(sprintf('Unable to read %s', $app['plugins.schema']));
}
return new static($app['plugins.json-validator'], $data, $app['phraseanet.version']);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Plugin\Schema;
use Alchemy\Phrasea\Plugin\Schema\ManifestValidator;
use Alchemy\Phrasea\Plugin\Schema\Manifest;
use Alchemy\Phrasea\Plugin\Exception\PluginValidationException;
use Alchemy\Phrasea\Plugin\Exception\JsonValidationException;
class PluginValidator
{
private $manifestValidator;
public function __construct(ManifestValidator $manifestValidator)
{
$this->manifestValidator = $manifestValidator;
}
public function validatePlugin($directory)
{
$this->ensureComposer($directory);
$this->ensureManifest($directory);
$manifest = $directory . DIRECTORY_SEPARATOR . 'manifest.json';
$data = @json_decode(@file_get_contents($manifest));
if (JSON_ERROR_NONE !== json_last_error()) {
throw new PluginValidationException(sprintf('Unable to parse file %s', $manifest));
}
try {
$this->manifestValidator->validate($data);
} catch (JsonValidationException $e) {
throw new PluginValidationException('Manifest file is invalid', $e->getCode(), $e);
}
return new Manifest($this->objectToArray($data));
}
private function ensureManifest($directory)
{
$manifest = $directory . DIRECTORY_SEPARATOR . 'manifest.json';
$this->ensureFile($manifest);
}
private function ensureComposer($directory)
{
$composer = $directory . DIRECTORY_SEPARATOR . 'composer.json';
$this->ensureFile($composer);
}
private function ensureFile($file)
{
if (!file_exists($file) || !is_file($file) || !is_readable($file)) {
throw new PluginValidationException(sprintf('Required file %s is not present.', $file));
}
}
private function objectToArray($data)
{
if (is_object($data)) {
$data = get_object_vars($data);
}
if (is_array($data)) {
return array_map(array($this, 'objectToArray'), $data);
}
return $data;
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "Class Connector",
"description" : "A custom class connector",
"keywords" : ["connector"],
"authors" : [
{
"name" : "Author name",
"homepage" : "http://example.com",
"email" : "email@example.com"
}
],
"homepage" : "http://example.com/project/example",
"license" : "MIT",
"version" : "0.1",
"minimum-phraseanet-version": "3.8",
"maximum-phraseanet-version": "3.9",
"services" : [
{
"class": "Vendor\\Connector\\PluginServiceInterface"
}
]
}

3
lib/autoload.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
return require __DIR__ . '/../plugins/autoload.php';

View File

@@ -0,0 +1,96 @@
{
"name": "Phraseanet Plugin Schema",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "The name of the plugin",
"required": true
},
"description": {
"type": "string",
"description": "The description of the plugin",
"required": true
},
"keywords": {
"type": "array",
"description": "An array of keywords",
"items": {
"type": "string",
"description": "A keyword"
}
},
"authors": {
"type": "array",
"description": "An array of authors",
"items": {
"type": "object",
"description": "An author",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "The author name",
"required": true
},
"email": {
"type": "string",
"description": "The author email",
"format": "email"
},
"homepage": {
"type": "string",
"description": "The author website URL",
"format": "uri"
}
}
}
},
"homepage": {
"type": "string",
"description": "The homepage of the plugin"
},
"license": {
"type": [
"string",
"array"
],
"description": "The license of the plugin"
},
"version": {
"type": "string",
"description": "The version of the plugin"
},
"minimum-phraseanet-version": {
"type": "string",
"description": "The minimum phraseanet version for the plugin, including the one provided."
},
"maximum-phraseanet-version": {
"type": "string",
"description": "The maximum phraseanet version for the plugin, excluding the one provided"
},
"services": {
"type": "array",
"description": "An array of services to register.",
"items": {
"type": "object",
"description": "A service",
"properties": {
"class": {
"type": "string",
"description": "The plugin service provider"
}
}
}
},
"extra": {
"type": [
"object",
"array"
],
"description": "Arbitrary extra data that can be used by custom installers.",
"additionalProperties": true
}
}
}

5
plugins/autoload.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
return call_user_func(function () {
return require __DIR__ . '/../vendor/autoload.php';
});

7
plugins/services.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
use Alchemy\Phrasea\Application;
return call_user_func(function (Application $app) {
return $app;
}, $app);

View File

@@ -0,0 +1,76 @@
<?php
namespace Alchemy\Tests\Phrasea\Command\Plugin;
use Alchemy\Phrasea\Command\Plugin\AddPlugin;
class AddPluginTest extends PluginCommandTestCase
{
public function testExecute()
{
$source = 'TestPlugin';
$input = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$input->expects($this->once())
->method('getArgument')
->with($this->equalTo('source'))
->will($this->returnValue($source));
$output = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
$command = new AddPlugin();
$command->setContainer(self::$DI['app']);
$manifest = $this->createManifestMock();
$manifest->expects($this->any())
->method('getName')
->will($this->returnValue($source));
self::$DI['app']['temporary-filesystem'] = $this->createTemporaryFilesystemMock();
self::$DI['app']['plugins.autoloader-generator'] = $this->createPluginsAutoloaderGeneratorMock();
self::$DI['app']['plugins.explorer'] = array(self::$DI['app']['plugins.directory'].'/TestPlugin');
self::$DI['app']['plugins.plugins-validator'] = $this->createPluginsValidatorMock();
self::$DI['app']['filesystem'] = $this->createFilesystemMock();
self::$DI['app']['plugins.composer-installer'] = $this->createComposerInstallerMock();
self::$DI['app']['plugins.importer'] = $this->createPluginsImporterMock();
self::$DI['app']['temporary-filesystem']->expects($this->once())
->method('createTemporaryDirectory')
->will($this->returnValue('tempdir'));
self::$DI['app']['plugins.importer']->expects($this->once())
->method('import')
->with($source, 'tempdir');
// the plugin is checked when updating config files
self::$DI['app']['plugins.plugins-validator']->expects($this->at(0))
->method('validatePlugin')
->with('tempdir')
->will($this->returnValue($manifest));
self::$DI['app']['plugins.plugins-validator']->expects($this->at(1))
->method('validatePlugin')
->with(self::$DI['app']['plugins.directory'].'/TestPlugin')
->will($this->returnValue($manifest));
self::$DI['app']['plugins.composer-installer']->expects($this->once())
->method('install')
->with('tempdir');
self::$DI['app']['filesystem']->expects($this->at(0))
->method('mirror')
->with('tempdir', self::$DI['app']['plugins.directory'].'/TestPlugin');
self::$DI['app']['filesystem']->expects($this->at(1))
->method('remove')
->with('tempdir');
self::$DI['app']['plugins.autoloader-generator']->expects($this->once())
->method('write')
->with(array($manifest));
$result = $command->execute($input, $output);
$this->assertSame(0, $result);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Alchemy\Tests\Phrasea\Command\Plugin;
class PluginCommandTestCase extends \PhraseanetPHPUnitAbstract
{
protected function createTemporaryFilesystemMock()
{
return $this->getMockBuilder('Neutron\TemporaryFilesystem\TemporaryFilesystem')
->disableOriginalConstructor()
->getMock();
}
protected function createPluginsImporterMock()
{
return $this->getMockBuilder('Alchemy\Phrasea\Plugin\Importer\Importer')
->disableOriginalConstructor()
->getMock();
}
protected function createPluginsValidatorMock()
{
return $this->getMockBuilder('Alchemy\Phrasea\Plugin\Schema\PluginValidator')
->disableOriginalConstructor()
->getMock();
}
protected function createManifestMock()
{
return $this->getMockBuilder('Alchemy\Phrasea\Plugin\Schema\Manifest')
->disableOriginalConstructor()
->getMock();
}
protected function createComposerInstallerMock()
{
return $this->getMockBuilder('Alchemy\Phrasea\Plugin\Management\ComposerInstaller')
->disableOriginalConstructor()
->getMock();
}
protected function createFilesystemMock()
{
return $this->getMockBuilder('Symfony\Component\Filesystem\Filesystem')
->disableOriginalConstructor()
->getMock();
}
protected function createPluginsExplorerMock()
{
return $this->getMockBuilder('Alchemy\Phrasea\Plugin\Management\PluginsExplorer')
->disableOriginalConstructor()
->getMock();
}
protected function createPluginsAutoloaderGeneratorMock()
{
return $this->getMockBuilder('Alchemy\Phrasea\Plugin\Management\AutoloaderGenerator')
->disableOriginalConstructor()
->getMock();
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Alchemy\Tests\Phrasea\Command\Plugin;
use Alchemy\Phrasea\Command\Plugin\RemovePlugin;
class RemovePluginTest extends PluginCommandTestCase
{
public function testExecute()
{
$name = 'TestPlugin';
$input = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$input->expects($this->once())
->method('getArgument')
->with($this->equalTo('name'))
->will($this->returnValue($name));
$output = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
$command = new RemovePlugin();
$command->setContainer(self::$DI['app']);
self::$DI['app']['filesystem'] = $this->createFilesystemMock();
self::$DI['app']['filesystem']->expects($this->once())
->method('remove')
->with(self::$DI['app']['plugins.directory'].'/'.$name);
$result = $command->execute($input, $output);
$this->assertSame(0, $result);
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Provider;
use Alchemy\Phrasea\Core\Provider\PluginServiceProvider;
use Silex\Application;
use Symfony\Component\Process\ExecutableFinder;
/**
* @covers Alchemy\Phrasea\Core\Provider\PluginServiceProvider
*/
class PluginServiceProvidertest extends ServiceProviderTestCase
{
public function provideServiceDescription()
{
return array(
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.json-validator',
'JsonSchema\Validator'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.plugins-validator',
'Alchemy\Phrasea\Plugin\Schema\PluginValidator'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.manifest-validator',
'Alchemy\Phrasea\Plugin\Schema\ManifestValidator'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.import-strategy',
'Alchemy\Phrasea\Plugin\Importer\ImportStrategy'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.autoloader-generator',
'Alchemy\Phrasea\Plugin\Management\AutoloaderGenerator'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.guzzle',
'Guzzle\Http\Client'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.composer-installer',
'Alchemy\Phrasea\Plugin\Management\ComposerInstaller'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.explorer',
'Alchemy\Phrasea\Plugin\Management\PluginsExplorer'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.importer',
'Alchemy\Phrasea\Plugin\Importer\Importer'
),
array(
'Alchemy\Phrasea\Core\Provider\PluginServiceProvider',
'plugins.importer.folder-importer',
'Alchemy\Phrasea\Plugin\Importer\FolderImporter'
),
);
}
public function testSchemaIsDefined()
{
$app = new Application();
$app->register(new PluginServiceProvider());
$this->assertFileExists($app['plugins.schema']);
$this->assertTrue(is_file($app['plugins.schema']));
}
public function testPluginDirIsDefined()
{
$app = new Application();
$app->register(new PluginServiceProvider());
$this->assertFileExists($app['plugins.directory']);
$this->assertTrue(is_dir($app['plugins.directory']));
}
public function testInstallerUsesPhpConf()
{
$finder = new ExecutableFinder();
$php = $finder->find('php');
if (null === $php) {
$this->markTestSkipped('Unable to detect PHP binary');
}
$app = new Application();
$app['phraseanet.registry'] = $this->createRegistryMock();
$app['phraseanet.registry']->expects($this->once())
->method('get')
->with('php_binary')
->will($this->returnValue($php));
$app->register(new PluginServiceProvider());
$this->assertInstanceOf('Alchemy\Phrasea\Plugin\Management\ComposerInstaller', $app['plugins.composer-installer']);
}
public function testInstallerCanDetectPhpConf()
{
$app = new Application();
$app['phraseanet.registry'] = $this->createRegistryMock();
$app['phraseanet.registry']->expects($this->once())
->method('get')
->with('php_binary')
->will($this->returnValue(null));
$app->register(new PluginServiceProvider());
$this->assertInstanceOf('Alchemy\Phrasea\Plugin\Management\ComposerInstaller', $app['plugins.composer-installer']);
}
private function createRegistryMock()
{
return $this->getMockBuilder('registry')
->disableOriginalConstructor()
->getMock();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Provider;
/**
* @covers Alchemy\Phrasea\Core\Provider\TemporaryFilesystemServiceProvider
*/
class TemporaryFilesystemServiceProvidertest extends ServiceProviderTestCase
{
public function provideServiceDescription()
{
return array(
array('Alchemy\Phrasea\Core\Provider\TemporaryFilesystemServiceProvider', 'temporary-filesystem', 'Neutron\TemporaryFilesystem\TemporaryFilesystem'),
);
}
}

View File

@@ -0,0 +1,10 @@
{
"name" : "test/test",
"description" : "test file",
"license" : "MIT",
"autoload": {
"psr-0": {
"Vendor" : "src"
}
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "TestPlugin",
"description" : "A custom class connector",
"keywords" : ["connector"],
"authors" : [
{
"name" : "Author name",
"homepage" : "http://example.com",
"email" : "email@example.com"
}
],
"homepage" : "http://example.com/project/example",
"license" : "MIT",
"version" : "0.1",
"minimum-phraseanet-version": "3.8",
"maximum-phraseanet-version": "3.9",
"services" : [
{
"class": "Vendor\\PluginService"
}
],
"extra" : {
"property" : "value"
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Vendor;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Silex\Application;
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
class PluginService implements PluginProviderInterface
{
public function register(Application $app)
{
$app['plugin-test'] = 'hello world';
}
public function boot(Application $app)
{
}
public static function create(PhraseaApplication $app)
{
return new static();
}
}

View File

@@ -0,0 +1,10 @@
{
"name" : "test/test",
"description" : "test file",
"license" : "MIT",
"autoload": {
"psr-0": {
"Vendor" : "src"
}
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "TestPlugin",
"description" : "A custom class connector",
"keywords" : ["connector"],
"authors" : [
{
"name" : "Author name",
"homepage" : "http://example.com",
"email" : "email@example.com"
}
],
"homepage" : "http://example.com/project/example",
"license" : "MIT",
"version" : "0.1",
"minimum-phraseanet-version": "3.8",
"maximum-phraseanet-version": "3.9",
"services" : [
{
"class": "Vendor\\PluginService"
}
]
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Vendor;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Silex\Application;
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
class PluginService implements PluginProviderInterface
{
public function register(Application $app)
{
$app['plugin-test'] = 'hello world';
}
public function boot(Application $app)
{
}
public static function create(PhraseaApplication $app)
{
return new static();
}
}

View File

@@ -0,0 +1,7 @@
<?php
// autoload.php generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit7a818310afafc4600ddb36d030b5d99d::getLoader();

View File

@@ -0,0 +1,246 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0 class loader
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class ClassLoader
{
private $prefixes = array();
private $fallbackDirs = array();
private $useIncludePath = false;
private $classMap = array();
public function getPrefixes()
{
return call_user_func_array('array_merge', $this->prefixes);
}
public function getFallbackDirs()
{
return $this->fallbackDirs;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of classes, merging with any others previously set.
*
* @param string $prefix The classes prefix
* @param array|string $paths The location(s) of the classes
* @param bool $prepend Prepend the location(s)
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirs = array_merge(
(array) $paths,
$this->fallbackDirs
);
} else {
$this->fallbackDirs = array_merge(
$this->fallbackDirs,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixes[$first][$prefix])) {
$this->prefixes[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixes[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixes[$first][$prefix]
);
} else {
$this->prefixes[$first][$prefix] = array_merge(
$this->prefixes[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of classes, replacing any others previously set.
*
* @param string $prefix The classes prefix
* @param array|string $paths The location(s) of the classes
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirs = (array) $paths;
return;
}
$this->prefixes[substr($prefix, 0, 1)][$prefix] = (array) $paths;
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
include $file;
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$classPath = strtr(substr($class, 0, $pos), '\\', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$className = substr($class, $pos + 1);
} else {
// PEAR-like class name
$classPath = null;
$className = $class;
}
$classPath .= strtr($className, '_', DIRECTORY_SEPARATOR) . '.php';
$first = $class[0];
if (isset($this->prefixes[$first])) {
foreach ($this->prefixes[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
return $dir . DIRECTORY_SEPARATOR . $classPath;
}
}
}
}
}
foreach ($this->fallbackDirs as $dir) {
if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
return $dir . DIRECTORY_SEPARATOR . $classPath;
}
}
if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) {
return $file;
}
return $this->classMap[$class] = false;
}
}

View File

@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Vendor\\PluginService' => $baseDir . '/src/Vendor/PluginService.php',
);

View File

@@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Vendor' => array($baseDir . '/src'),
);

View File

@@ -0,0 +1,43 @@
<?php
// autoload_real.php generated by Composer
class ComposerAutoloaderInit7a818310afafc4600ddb36d030b5d99d
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit7a818310afafc4600ddb36d030b5d99d', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit7a818310afafc4600ddb36d030b5d99d', 'loadClassLoader'));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
$loader->register(true);
return $loader;
}
}

View File

@@ -0,0 +1,10 @@
{
"name" : "test/test",
"description" : "test file",
"license" : "MIT",
"autoload": {
"psr-0": {
"Vendor" : "src"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Test Plugin",
"description" : "A custom class connector",
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Vendor;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Silex\Application;
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
class PluginService implements PluginProviderInterface
{
public function register(Application $app)
{
$app['plugin-test'] = 'hello world';
}
public function boot(Application $app)
{
}
public static function create(PhraseaApplication $app)
{
return new static();
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "TestPlugin",
"description" : "A custom class connector",
"keywords" : ["connector"],
"authors" : [
{
"name" : "Author name",
"homepage" : "http://example.com",
"email" : "email@example.com"
}
],
"homepage" : "http://example.com/project/example",
"license" : "MIT",
"version" : "0.1",
"minimum-phraseanet-version": "3.8",
"maximum-phraseanet-version": "3.9",
"services" : [
{
"class": "Vendor\\PluginService"
}
],
"extra" : {
"property" : "value"
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Vendor;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Silex\Application;
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
class PluginService implements PluginProviderInterface
{
public function register(Application $app)
{
$app['plugin-test'] = 'hello world';
}
public function boot(Application $app)
{
}
public static function create(PhraseaApplication $app)
{
return new static();
}
}

View File

@@ -0,0 +1,10 @@
{
"name" : "test/test",
"description" : "test file",
"license" : "MIT",
"autoload": {
"psr-0": {
"Vendor" : "src"
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Vendor;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Silex\Application;
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
class PluginService implements PluginProviderInterface
{
public function register(Application $app)
{
$app['plugin-test'] = 'hello world';
}
public function boot(Application $app)
{
}
public static function create(PhraseaApplication $app)
{
return new static();
}
}

View File

@@ -0,0 +1,10 @@
{
"name" : "test/test",
"description" : "test file",
"license" : "MIT",
"autoload": {
"psr-0": {
"Vendor" : "src"
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Vendor;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Silex\Application;
use Alchemy\Phrasea\Plugin\PluginProviderInterface;
class PluginService implements PluginProviderInterface
{
public function register(Application $app)
{
$app['plugin-test'] = 'hello world';
}
public function boot(Application $app)
{
}
public static function create(PhraseaApplication $app)
{
return new static();
}
}

View File

@@ -0,0 +1,33 @@
{
"name": "TestPlugin",
"description" : "A custom class connector",
"keywords" : ["connector", "test"],
"authors" : [
{
"name" : "Author name",
"homepage" : "http://example.com",
"email" : "email@example.com"
},
{
"name" : "Author name2",
"homepage" : "http://example.com",
"email" : "email@example.com"
}
],
"homepage" : "http://example.com/project/example",
"license" : "MIT",
"version" : "0.1",
"minimum-phraseanet-version": "3.8",
"maximum-phraseanet-version": "3.9",
"services" : [
{
"class": "Vendor\\PluginService"
},
{
"class": "Vendor\\PluginService2"
}
],
"extra" : {
"property" : "value"
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "TestPlugin",
"description" : "A custom class connector"
}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,3 @@
{
"description": "pretty one"
}

View File

@@ -0,0 +1,12 @@
{
"name": "TestPlugin",
"description" : "A custom class connector",
"authors" : {
"name" : "Author name",
"homepage" : "http://example.com",
"email" : "email@example.com"
},
"services" : {
"class": "Vendor\\PluginService"
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "Test Plugin",
"description" : "A custom class connector"
}

View File

@@ -0,0 +1,5 @@
{
"name": "TestPlugin",
"description" : "A custom class connector",
"minimum-phraseanet-version": "14"
}

View File

@@ -0,0 +1,5 @@
{
"name": "TestPlugin",
"description" : "A custom class connector",
"maximum-phraseanet-version": "3.8"
}

View File

@@ -0,0 +1,7 @@
<?php
// autoload.php generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit4ea6c38a75e30f622d666844c915395f::getLoader();

View File

@@ -0,0 +1,246 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0 class loader
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class ClassLoader
{
private $prefixes = array();
private $fallbackDirs = array();
private $useIncludePath = false;
private $classMap = array();
public function getPrefixes()
{
return call_user_func_array('array_merge', $this->prefixes);
}
public function getFallbackDirs()
{
return $this->fallbackDirs;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of classes, merging with any others previously set.
*
* @param string $prefix The classes prefix
* @param array|string $paths The location(s) of the classes
* @param bool $prepend Prepend the location(s)
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirs = array_merge(
(array) $paths,
$this->fallbackDirs
);
} else {
$this->fallbackDirs = array_merge(
$this->fallbackDirs,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixes[$first][$prefix])) {
$this->prefixes[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixes[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixes[$first][$prefix]
);
} else {
$this->prefixes[$first][$prefix] = array_merge(
$this->prefixes[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of classes, replacing any others previously set.
*
* @param string $prefix The classes prefix
* @param array|string $paths The location(s) of the classes
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirs = (array) $paths;
return;
}
$this->prefixes[substr($prefix, 0, 1)][$prefix] = (array) $paths;
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
include $file;
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$classPath = strtr(substr($class, 0, $pos), '\\', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$className = substr($class, $pos + 1);
} else {
// PEAR-like class name
$classPath = null;
$className = $class;
}
$classPath .= strtr($className, '_', DIRECTORY_SEPARATOR) . '.php';
$first = $class[0];
if (isset($this->prefixes[$first])) {
foreach ($this->prefixes[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
return $dir . DIRECTORY_SEPARATOR . $classPath;
}
}
}
}
}
foreach ($this->fallbackDirs as $dir) {
if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
return $dir . DIRECTORY_SEPARATOR . $classPath;
}
}
if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) {
return $file;
}
return $this->classMap[$class] = false;
}
}

View File

@@ -0,0 +1,9 @@
<?php
// autoload_classmap.php generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,43 @@
<?php
// autoload_real.php generated by Composer
class ComposerAutoloaderInit4ea6c38a75e30f622d666844c915395f
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit4ea6c38a75e30f622d666844c915395f', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit4ea6c38a75e30f622d666844c915395f', 'loadClassLoader'));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
$loader->register(true);
return $loader;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Importer;
use Alchemy\Phrasea\Plugin\Importer\FolderImporter;
use Alchemy\Tests\Phrasea\Plugin\PluginTestCase;
use Symfony\Component\Filesystem\Exception\IOException;
class FolderImporterTest extends PluginTestCase
{
public function testImport()
{
$fs = $this->createFilesystemMock();
$source = 'test-plugin';
$target = __DIR__;
$fs->expects($this->once())
->method('mirror')
->with($source, $target);
$importer = new FolderImporter($fs);
$importer->import($source, $target);
}
/**
* @expectedException Alchemy\Phrasea\Plugin\Exception\ImportFailureException
*/
public function testImportFailed()
{
$fs = $this->createFilesystemMock();
$source = 'test-plugin';
$target = __DIR__;
$fs->expects($this->once())
->method('mirror')
->with($source, $target)
->will($this->throwException(new IOException('Error')));
$importer = new FolderImporter($fs);
$importer->import($source, $target);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Importer;
use Alchemy\Phrasea\Plugin\Importer\ImportStrategy;
use Alchemy\Tests\Phrasea\Plugin\PluginTestCase;
class ImportStrategyTest extends PluginTestCase
{
/**
* @dataProvider provideFolderSources
*/
public function testDetect($source)
{
$importer = new ImportStrategy();
$this->assertEquals('plugins.importer.folder-importer', $importer->detect($source));
}
/**
* @dataProvider provideInvalidFolderSources
* @expectedException Alchemy\Phrasea\Plugin\Exception\ImportFailureException
*/
public function testDetectFailure($source)
{
$importer = new ImportStrategy();
$importer->detect($source);
}
public function provideFolderSources()
{
return array(
array(__DIR__),
array(dirname(__DIR__)),
);
}
public function provideInvalidFolderSources()
{
return array(
array('/path/to/invalid/dir'),
array(__FILE__),
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Importer;
use Alchemy\Phrasea\Plugin\Importer\Importer;
use Alchemy\Tests\Phrasea\Plugin\PluginTestCase;
class ImporterTest extends PluginTestCase
{
public function testImport()
{
$source = 'here';
$target = 'there';
$strategy = $this->getMock('Alchemy\Phrasea\Plugin\Importer\ImportStrategy');
$strategy->expects($this->once())
->method('detect')
->with($source)
->will($this->returnValue('elephant'));
$importerInterface = $this->getMock('Alchemy\Phrasea\Plugin\Importer\ImporterInterface');
$importerInterface->expects($this->once())
->method('import')
->with($source, $target);
$importer = new Importer($strategy, array('elephant' => $importerInterface));
$importer->import($source, $target);
}
/**
* @expectedException Alchemy\Phrasea\Plugin\Exception\ImportFailureException
*/
public function testImportFailure()
{
$source = 'here';
$target = 'there';
$strategy = $this->getMock('Alchemy\Phrasea\Plugin\Importer\ImportStrategy');
$strategy->expects($this->once())
->method('detect')
->with($source)
->will($this->returnValue('elephant'));
$importerInterface = $this->getMock('Alchemy\Phrasea\Plugin\Importer\ImporterInterface');
$importerInterface->expects($this->never())
->method('import');
$importer = new Importer($strategy, array('rhinoceros' => $importerInterface));
$importer->import($source, $target);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Management;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Plugin\Management\AutoloaderGenerator;
use Alchemy\Phrasea\Plugin\Schema\Manifest;
use Symfony\Component\Process\ProcessBuilder;
use Symfony\Component\Process\ExecutableFinder;
class AutoloaderGeneratorTest extends \PHPUnit_Framework_TestCase
{
public function testGeneratedFileAfterInstall()
{
$pluginDir = __DIR__ . '/../Fixtures/PluginDirInstalled/TestPlugin';
$pluginsDir = __DIR__ . '/../Fixtures/PluginDirInstalled';
$files = array($pluginsDir . '/services.php', $pluginsDir . '/autoload.php');
$this->cleanup($files);
$generator = new AutoloaderGenerator($pluginsDir);
$generator->write(array(new Manifest(json_decode(file_get_contents($pluginDir . '/manifest.json'), true))));
$finder = new ExecutableFinder();
$php = $finder->find('php');
if (null === $php) {
$this->markTestSkipped('Php executable not found.');
}
foreach ($files as $file ) {
$this->assertFileExists($file);
$process = ProcessBuilder::create(array($php, '-l', $file))->getProcess();
$process->run();
$this->assertTrue($process->isSuccessful(), basename($file) . ' is valid');
}
// test autoload
$this->assertFalse(class_exists('Vendor\PluginService'));
$loader = require $pluginsDir . '/autoload.php';
$this->assertInstanceOf('Composer\Autoload\ClassLoader', $loader);
$this->assertTrue(class_exists('Vendor\PluginService'));
// load services
$app = new Application();
$retrievedApp = require $pluginsDir . '/services.php';
$this->assertSame($app, $retrievedApp);
$this->assertEquals('hello world', $app['plugin-test']);
$this->cleanup($files);
}
private function cleanup($files)
{
foreach ($files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Alchemy\Phrasea\Plugin\Management;
use Alchemy\Phrasea\Plugin\Management\ComposerInstaller;
use Guzzle\Http\Client as Guzzle;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Filesystem\Filesystem;
class ComposerInstallerTest extends \PHPUnit_Framework_TestCase
{
public function testInstall()
{
$fs = new Filesystem();
$finder = new ExecutableFinder();
$php = $finder->find('php');
$vendorDir = __DIR__ . '/../Fixtures/PluginDir/TestPlugin/vendor';
$installFile = __DIR__ . '/installer';
$composer = __DIR__ . '/composer.phar';
$fs->remove(array($composer, $installFile, $vendorDir));
if (null === $php) {
$this->markTestSkipped('Unable to find PHP executable.');
}
$installer = new ComposerInstaller(__DIR__, new Guzzle(), $php);
$installer->install(__DIR__ . '/../Fixtures/PluginDir/TestPlugin');
$this->assertFileExists($composer);
unlink($composer);
$this->assertFileNotExists($installFile);
$this->assertFileExists($vendorDir);
$fs->remove($vendorDir);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Management;
use Alchemy\Phrasea\Plugin\Management\PluginsExplorer;
use Alchemy\Tests\Phrasea\Plugin\PluginTestCase;
class PluginsExplorerTest extends PluginTestCase
{
public function testCount()
{
$explorer = new PluginsExplorer(__DIR__ . '/../Fixtures/PluginDir');
$this->assertCount(1, $explorer);
}
public function testGetIterator()
{
$explorer = new PluginsExplorer(__DIR__ . '/../Fixtures/PluginDir');
$dirs = array();
foreach ($explorer as $dir) {
$dirs[] = (string) realpath($dir);
}
$this->assertSame(array(realpath(__DIR__ . '/../Fixtures/PluginDir/TestPlugin')), $dirs);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin;
use Alchemy\Phrasea\Plugin\Schema\ManifestValidator;
class PluginTestCase extends \PhraseanetPHPUnitAbstract
{
protected function createManifestValidator()
{
return ManifestValidator::create(self::$DI['app']);
}
protected function getPluginDirectory()
{
return __DIR__ . DIRECTORY_SEPARATOR . 'PluginFolder';
}
protected function createFilesystemMock()
{
return $this->getMock('Symfony\Component\Filesystem\Filesystem');
}
protected function getSchema()
{
return file_get_contents($this->getSchemaPath());
}
protected function getSchemaPath()
{
return __DIR__ . '/../../../../../lib/conf.d/plugin-schema.json';
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Schema;
use Alchemy\Phrasea\Plugin\Schema\Manifest;
class ManifestTest extends \PHPUnit_Framework_TestCase
{
public function testGetters()
{
$data = json_decode(file_get_contents(__DIR__ . '/../Fixtures/PluginDir/TestPlugin/manifest.json'), true);
$manifest = new Manifest($data);
$this->assertEquals('TestPlugin', $manifest->getName());
$this->assertEquals('A custom class connector', $manifest->getDescription());
$this->assertEquals(array('connector'), $manifest->getKeywords());
$this->assertEquals(array(array(
'name' => 'Author name',
'homepage' => 'http://example.com',
'email' => 'email@example.com',
)), $manifest->getAuthors());
$this->assertEquals('http://example.com/project/example', $manifest->getHomepage());
$this->assertEquals('MIT', $manifest->getLicense());
$this->assertEquals('0.1', $manifest->getVersion());
$this->assertEquals('3.8', $manifest->getMinimumPhraseanetVersion());
$this->assertEquals('3.9', $manifest->getMaximumPhraseanetVersion());
$this->assertEquals(array(array('class' => 'Vendor\PluginService')), $manifest->getServices());
$this->assertEquals(array('property' => 'value'), $manifest->getExtra());
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Schema;
use Alchemy\Phrasea\Plugin\Schema\ManifestValidator;
use JsonSchema\Validator as JsonSchemaValidator;
use Alchemy\Tests\Phrasea\Plugin\PluginTestCase;
class ManifestValidatorTest extends PluginTestCase
{
/**
* @dataProvider provideGoodManifestFiles
*/
public function testValidateGoodOnes($file)
{
$validator = $this->createValidator();
$validator->validate(json_decode(file_get_contents($file)));
}
public function provideGoodManifestFiles()
{
return array(
array(__DIR__ . '/../Fixtures/manifest-good-big.json'),
array(__DIR__ . '/../Fixtures/manifest-good-minimal.json'),
);
}
/**
* @expectedException Alchemy\Phrasea\Plugin\Exception\JsonValidationException
* @dataProvider provideWrongManifestFiles
*/
public function testValidateWrongOnes($file)
{
$validator = $this->createValidator();
$validator->validate(json_decode(file_get_contents($file)));
}
public function provideWrongManifestFiles()
{
return array(
array(__DIR__ . '/../Fixtures/manifest-wrong1.json'),
array(__DIR__ . '/../Fixtures/manifest-wrong2.json'),
array(__DIR__ . '/../Fixtures/manifest-wrong3.json'),
array(__DIR__ . '/../Fixtures/manifest-wrong4.json'),
array(__DIR__ . '/../Fixtures/manifest-wrong5-min-version.json'),
array(__DIR__ . '/../Fixtures/manifest-wrong6-max-version.json'),
);
}
/**
* @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException
*/
public function testValidateInvalidData()
{
$validator = $this->createValidator();
$validator->validate(array());
}
/**
* @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException
*/
public function testConstructWithInvalidSchema()
{
new ManifestValidator(new JsonSchemaValidator(), array(), self::$DI['app']['phraseanet.version']);
}
public function testCreate()
{
$validator = ManifestValidator::create(self::$DI['app']);
$this->assertInstanceOf('Alchemy\Phrasea\Plugin\Schema\ManifestValidator', $validator);
}
private function createValidator()
{
$schema = json_decode($this->getSchema());
return new ManifestValidator(new JsonSchemaValidator(), $schema, self::$DI['app']['phraseanet.version']);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Alchemy\Tests\Phrasea\Plugin\Schema;
use Alchemy\Phrasea\Plugin\Schema\PluginValidator;
use Alchemy\Phrasea\Plugin\Schema\ManifestValidator;
use JsonSchema\Validator as JsonValidator;
use Alchemy\Tests\Phrasea\Plugin\PluginTestCase;
class PluginValidatorTest extends PluginTestCase
{
/**
* @dataProvider provideInvalidPluginDirs
* @expectedException Alchemy\Phrasea\Plugin\Exception\PluginValidationException
*/
public function testValidateInvalidPlugin($directory)
{
$validator = new PluginValidator($this->createManifestValidator());
$validator->validatePlugin($directory);
}
/**
* @dataProvider providePluginDirs
*/
public function testValidatePlugin($directory)
{
$validator = new PluginValidator($this->createManifestValidator());
$validator->validatePlugin($directory);
}
public function providePluginDirs()
{
return array(
array(__DIR__ . '/../Fixtures/PluginDir/TestPlugin'),
);
}
public function provideInvalidPluginDirs()
{
return array(
array(__DIR__ . '/../Fixtures/WrongPlugins/TestPluginInvalidManifest'),
array(__DIR__ . '/../Fixtures/WrongPlugins/TestPluginMissingComposer'),
array(__DIR__ . '/../Fixtures/WrongPlugins/TestPluginMissingManifest'),
array(__DIR__ . '/../Fixtures/WrongPlugins/TestPluginWrongManifest'),
);
}
}

View File

@@ -1,5 +1,5 @@
<?php
$loader = require __DIR__ . '/../vendor/autoload.php';
$loader = require __DIR__ . '/../lib/autoload.php';
$loader->add('Alchemy\\Tests', __DIR__);
$loader->add('', __DIR__ . "/classes");

View File

@@ -14,7 +14,7 @@
* @license http://opensource.org/licenses/gpl-3.0 GPLv3
* @link www.phraseanet.com
*/
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/autoload.php';
$app = require __DIR__ . '/../lib/Alchemy/Phrasea/Application/Api.php';

View File

@@ -14,7 +14,7 @@
* @license http://opensource.org/licenses/gpl-3.0 GPLv3
* @link www.phraseanet.com
*/
require_once __DIR__ . "/../vendor/autoload.php";
require_once __DIR__ . "/../lib/autoload.php";
$environment = 'prod';
$app = require __DIR__ . '/../lib/Alchemy/Phrasea/Application/Root.php';

View File

@@ -14,7 +14,7 @@
* @license http://opensource.org/licenses/gpl-3.0 GPLv3
* @link www.phraseanet.com
*/
require_once __DIR__ . "/../vendor/autoload.php";
require_once __DIR__ . "/../lib/autoload.php";
$environment = 'dev';
$app = require __DIR__ . '/../lib/Alchemy/Phrasea/Application/Root.php';