From 918f60d8162c36d70ce5626102204bd71bb3f22e Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 00:01:29 +0200 Subject: [PATCH 01/13] Add Fixtures --- .../PluginDir/TestPlugin/composer.json | 10 + .../PluginDir/TestPlugin/manifest.json | 25 ++ .../TestPlugin/src/Vendor/PluginService.php | 24 ++ .../TestPlugin/composer.json | 10 + .../TestPlugin/manifest.json | 22 ++ .../TestPlugin/src/Vendor/PluginService.php | 24 ++ .../TestPlugin/vendor/autoload.php | 7 + .../vendor/composer/ClassLoader.php | 246 ++++++++++++++++++ .../vendor/composer/autoload_classmap.php | 10 + .../vendor/composer/autoload_namespaces.php | 10 + .../vendor/composer/autoload_real.php | 43 +++ .../TestPluginInvalidManifest/composer.json | 10 + .../TestPluginInvalidManifest/manifest.json | 4 + .../src/Vendor/PluginService.php | 24 ++ .../TestPluginMissingComposer/manifest.json | 25 ++ .../src/Vendor/PluginService.php | 24 ++ .../TestPluginMissingManifest/composer.json | 10 + .../src/Vendor/PluginService.php | 24 ++ .../TestPluginWrongManifest/composer.json | 10 + .../TestPluginWrongManifest/manifest.json | 1 + .../src/Vendor/PluginService.php | 24 ++ .../Plugin/Fixtures/manifest-good-big.json | 33 +++ .../Fixtures/manifest-good-minimal.json | 4 + .../Plugin/Fixtures/manifest-wrong1.json | 2 + .../Plugin/Fixtures/manifest-wrong2.json | 3 + .../Plugin/Fixtures/manifest-wrong3.json | 12 + .../Plugin/Fixtures/manifest-wrong4.json | 4 + .../Plugin/Fixtures/vendor/autoload.php | 7 + .../Fixtures/vendor/composer/ClassLoader.php | 246 ++++++++++++++++++ .../vendor/composer/autoload_classmap.php | 9 + .../vendor/composer/autoload_namespaces.php | 9 + .../vendor/composer/autoload_real.php | 43 +++ 32 files changed, 959 insertions(+) create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/composer.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/manifest.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/src/Vendor/PluginService.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/composer.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/manifest.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/src/Vendor/PluginService.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/autoload.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/ClassLoader.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_classmap.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_namespaces.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_real.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/composer.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/manifest.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/src/Vendor/PluginService.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginMissingComposer/manifest.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginMissingComposer/src/Vendor/PluginService.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginMissingManifest/composer.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginMissingManifest/src/Vendor/PluginService.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginWrongManifest/composer.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginWrongManifest/manifest.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginWrongManifest/src/Vendor/PluginService.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-good-big.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-good-minimal.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong1.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong2.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong3.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong4.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/autoload.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/composer/ClassLoader.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/composer/autoload_classmap.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/composer/autoload_namespaces.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/composer/autoload_real.php diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/composer.json b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/composer.json new file mode 100644 index 0000000000..9e71751641 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/composer.json @@ -0,0 +1,10 @@ +{ + "name" : "test/test", + "description" : "test file", + "license" : "MIT", + "autoload": { + "psr-0": { + "Vendor" : "src" + } + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/manifest.json b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/manifest.json new file mode 100644 index 0000000000..c732805a54 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/manifest.json @@ -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" + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/src/Vendor/PluginService.php b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/src/Vendor/PluginService.php new file mode 100644 index 0000000000..19b9dfd44a --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/src/Vendor/PluginService.php @@ -0,0 +1,24 @@ + + * Jordi Boggiano + * + * 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 + * @author Jordi Boggiano + */ +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; + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_classmap.php b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000000..d3ef7827a3 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $baseDir . '/src/Vendor/PluginService.php', +); diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_namespaces.php b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000000..43ccccf9a9 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_namespaces.php @@ -0,0 +1,10 @@ + array($baseDir . '/src'), +); diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_real.php b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_real.php new file mode 100644 index 0000000000..10cf2abce2 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/vendor/composer/autoload_real.php @@ -0,0 +1,43 @@ + $path) { + $loader->set($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + return $loader; + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/composer.json b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/composer.json new file mode 100644 index 0000000000..9e71751641 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/composer.json @@ -0,0 +1,10 @@ +{ + "name" : "test/test", + "description" : "test file", + "license" : "MIT", + "autoload": { + "psr-0": { + "Vendor" : "src" + } + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/manifest.json b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/manifest.json new file mode 100644 index 0000000000..b4b5f747ca --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "Test Plugin", + "description" : "A custom class connector", +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/src/Vendor/PluginService.php b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/src/Vendor/PluginService.php new file mode 100644 index 0000000000..19b9dfd44a --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/WrongPlugins/TestPluginInvalidManifest/src/Vendor/PluginService.php @@ -0,0 +1,24 @@ + + * Jordi Boggiano + * + * 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 + * @author Jordi Boggiano + */ +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; + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/composer/autoload_classmap.php b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000000..af4ad5805a --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + $path) { + $loader->set($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + return $loader; + } +} From 3601fddbb9aa23c1a7b02ed1a745e79a662891cc Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 14:20:06 +0200 Subject: [PATCH 02/13] Add plugins directory to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2eddb0df63..6f0aad4e82 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /config/connexions.yml .DS_Store /vendor +/plugins composer.phar behat.yml /datas From 68f9446d2234aea02ff7dd9d4cf39e14371cd03c Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 02:27:45 +0200 Subject: [PATCH 03/13] Add autoloader.php and services.php --- plugins/autoload.php | 5 +++++ plugins/services.php | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 plugins/autoload.php create mode 100644 plugins/services.php diff --git a/plugins/autoload.php b/plugins/autoload.php new file mode 100644 index 0000000000..e914fb7f97 --- /dev/null +++ b/plugins/autoload.php @@ -0,0 +1,5 @@ + Date: Thu, 30 May 2013 13:02:00 +0200 Subject: [PATCH 04/13] Add base plugin files --- .../Core/Provider/PluginServiceProvider.php | 85 ++++++++++++ .../Exception/ComposerInstallException.php | 18 +++ .../Exception/ImportFailureException.php | 18 +++ .../Exception/JsonValidationException.php | 30 ++++ .../Exception/PluginValidationException.php | 18 +++ .../RegistrationFailureException.php | 18 +++ .../Plugin/Importer/FolderImporter.php | 44 ++++++ .../Plugin/Importer/ImportStrategy.php | 27 ++++ .../Phrasea/Plugin/Importer/Importer.php | 44 ++++++ .../Plugin/Importer/ImporterInterface.php | 25 ++++ .../Plugin/Management/AutoloaderGenerator.php | 81 +++++++++++ .../Plugin/Management/ComposerInstaller.php | 124 +++++++++++++++++ .../Plugin/Management/PluginsExplorer.php | 47 +++++++ .../Plugin/PluginProviderInterface.php | 29 ++++ .../Phrasea/Plugin/Schema/Manifest.php | 82 +++++++++++ .../Plugin/Schema/ManifestValidator.php | 67 +++++++++ .../Phrasea/Plugin/Schema/PluginValidator.php | 68 +++++++++ .../Phrasea/Plugin/resources/example.json | 22 +++ lib/conf.d/plugin-schema.json | 96 +++++++++++++ .../Provider/PluginServiceProviderTest.php | 129 ++++++++++++++++++ .../Plugin/Importer/FolderImporterTest.php | 44 ++++++ .../Plugin/Importer/ImportStrategyTest.php | 44 ++++++ .../Phrasea/Plugin/Importer/ImporterTest.php | 51 +++++++ .../Management/AutoloaderGeneratorTest.php | 63 +++++++++ .../Management/ComposerInstallerTest.php | 40 ++++++ .../Plugin/Management/PluginsExplorerTest.php | 29 ++++ .../Tests/Phrasea/Plugin/PluginTestCase.php | 26 ++++ .../Phrasea/Plugin/Schema/ManifestTest.php | 30 ++++ .../Plugin/Schema/ManifestValidatorTest.php | 78 +++++++++++ .../Plugin/Schema/PluginValidatorTest.php | 51 +++++++ 30 files changed, 1528 insertions(+) create mode 100644 lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Exception/ComposerInstallException.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Exception/ImportFailureException.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Exception/JsonValidationException.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Exception/PluginValidationException.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Exception/RegistrationFailureException.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Importer/FolderImporter.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Importer/ImportStrategy.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Importer/Importer.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Importer/ImporterInterface.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Management/AutoloaderGenerator.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Management/ComposerInstaller.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Management/PluginsExplorer.php create mode 100644 lib/Alchemy/Phrasea/Plugin/PluginProviderInterface.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Schema/Manifest.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Schema/ManifestValidator.php create mode 100644 lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php create mode 100644 lib/Alchemy/Phrasea/Plugin/resources/example.json create mode 100644 lib/conf.d/plugin-schema.json create mode 100644 tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Importer/FolderImporterTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImportStrategyTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImporterTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Management/ComposerInstallerTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Management/PluginsExplorerTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php diff --git a/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php new file mode 100644 index 0000000000..6207f94ab4 --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php @@ -0,0 +1,85 @@ +share(function (Application $app) { + return new JsonValidator(); + }); + + $app['plugins.manifest-validator'] = $app->share(function (Application $app) { + return ManifestValidator::create($app['json-validator'], $app['plugins.schema']); + }); + + $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) { + if (null === $phpBinary = $app['phraseanet.registry']->get('php_binary')) { + $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) + { + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Exception/ComposerInstallException.php b/lib/Alchemy/Phrasea/Plugin/Exception/ComposerInstallException.php new file mode 100644 index 0000000000..a4606f63a5 --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Exception/ComposerInstallException.php @@ -0,0 +1,18 @@ +errors = $errors; + parent::__construct($message); + } + + public function getErrors() + { + return $this->errors; + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Exception/PluginValidationException.php b/lib/Alchemy/Phrasea/Plugin/Exception/PluginValidationException.php new file mode 100644 index 0000000000..b00aff0543 --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Exception/PluginValidationException.php @@ -0,0 +1,18 @@ +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); + } + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Importer/ImportStrategy.php b/lib/Alchemy/Phrasea/Plugin/Importer/ImportStrategy.php new file mode 100644 index 0000000000..3c58814645 --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Importer/ImportStrategy.php @@ -0,0 +1,27 @@ +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); + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Importer/ImporterInterface.php b/lib/Alchemy/Phrasea/Plugin/Importer/ImporterInterface.php new file mode 100644 index 0000000000..1a87e708ae --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Importer/ImporterInterface.php @@ -0,0 +1,25 @@ +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 = "getName() . DIRECTORY_SEPARATOR . "vendor" . DIRECTORY_SEPARATOR . "autoload.php';\n"; + } + + // composer loader are preprent + $buffer .= " \$loader = require __DIR__ . '/../vendor/autoload.php';\n"; + + $buffer .= "\n return \$loader;\n});\n"; + + return $buffer; + } + + private function createServices($manifests) + { + $buffer = "getServices() as $service) { + $buffer .= " \$app->register(\\".$service['class']."::create(\$app));\n"; + } + } + + $buffer .= "\n return \$app;\n}, \$app);\n"; + + return $buffer; + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Management/ComposerInstaller.php b/lib/Alchemy/Phrasea/Plugin/Management/ComposerInstaller.php new file mode 100644 index 0000000000..8169565d65 --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Management/ComposerInstaller.php @@ -0,0 +1,124 @@ +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.'); + } + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Management/PluginsExplorer.php b/lib/Alchemy/Phrasea/Plugin/Management/PluginsExplorer.php new file mode 100644 index 0000000000..ba8d5296ab --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Management/PluginsExplorer.php @@ -0,0 +1,47 @@ +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); + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/PluginProviderInterface.php b/lib/Alchemy/Phrasea/Plugin/PluginProviderInterface.php new file mode 100644 index 0000000000..b7dde714fb --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/PluginProviderInterface.php @@ -0,0 +1,29 @@ +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; + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Schema/ManifestValidator.php b/lib/Alchemy/Phrasea/Plugin/Schema/ManifestValidator.php new file mode 100644 index 0000000000..9676ca3d43 --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Schema/ManifestValidator.php @@ -0,0 +1,67 @@ +validator = $validator; + $this->schemaData = $schemaData; + } + + public function validate($data) + { + if (!is_object($data)) { + throw new InvalidArgumentException('Json Schema must be an object'); + } + + $validator = clone $this->validator; + $validator->check($data, $this->schemaData); + + if (!$validator->isValid()) { + $errors = array(); + foreach ((array) $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')); + } + + // validate gainst versions + } + + public static function create(JsonValidator $jsonValidator, $path) + { + $data = @json_decode(@file_get_contents($path)); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new InvalidArgumentException(sprintf('Unable to read %s', $path)); + } + + return new static($jsonValidator, $data); + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php b/lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php new file mode 100644 index 0000000000..f28445c69c --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php @@ -0,0 +1,68 @@ +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); + } + + // a ameliorer + return new Manifest(@json_decode(@file_get_contents($manifest), true)); + } + + 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)); + } + } +} diff --git a/lib/Alchemy/Phrasea/Plugin/resources/example.json b/lib/Alchemy/Phrasea/Plugin/resources/example.json new file mode 100644 index 0000000000..5f0116601e --- /dev/null +++ b/lib/Alchemy/Phrasea/Plugin/resources/example.json @@ -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" + } + ] +} diff --git a/lib/conf.d/plugin-schema.json b/lib/conf.d/plugin-schema.json new file mode 100644 index 0000000000..bd772efede --- /dev/null +++ b/lib/conf.d/plugin-schema.json @@ -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 + } + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php new file mode 100644 index 0000000000..076f29fc00 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php @@ -0,0 +1,129 @@ +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(); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Importer/FolderImporterTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Importer/FolderImporterTest.php new file mode 100644 index 0000000000..5e04521a26 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Importer/FolderImporterTest.php @@ -0,0 +1,44 @@ +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); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImportStrategyTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImportStrategyTest.php new file mode 100644 index 0000000000..c394bb0cca --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImportStrategyTest.php @@ -0,0 +1,44 @@ +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__), + ); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImporterTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImporterTest.php new file mode 100644 index 0000000000..aa0bbd9f0e --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Importer/ImporterTest.php @@ -0,0 +1,51 @@ +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); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php new file mode 100644 index 0000000000..8ee8241189 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php @@ -0,0 +1,63 @@ +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()); + } + + // 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); + } + } + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Management/ComposerInstallerTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Management/ComposerInstallerTest.php new file mode 100644 index 0000000000..abe7bd7671 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Management/ComposerInstallerTest.php @@ -0,0 +1,40 @@ +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); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Management/PluginsExplorerTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Management/PluginsExplorerTest.php new file mode 100644 index 0000000000..f5565c9d23 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Management/PluginsExplorerTest.php @@ -0,0 +1,29 @@ +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); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php b/tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php new file mode 100644 index 0000000000..8603550843 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php @@ -0,0 +1,26 @@ +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'; + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestTest.php new file mode 100644 index 0000000000..07bdc72495 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestTest.php @@ -0,0 +1,30 @@ +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()); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php new file mode 100644 index 0000000000..dae749933a --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php @@ -0,0 +1,78 @@ +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'), + ); + } + + /** + * @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException + */ + public function testValidateInvalidData() + { + $validator = $this->createValidator(); + $validator->validate(array()); + } + + /** + * @expectedException Alchemy\Phrasea\Exception\InvalidArgumentException + */ + public function testConstructWithInvalidSchema() + { + $validator = new ManifestValidator(new JsonSchemaValidator(), array()); + } + + public function testCreate() + { + $validator = ManifestValidator::create(new JsonSchemaValidator(), $this->getSchemaPath()); + + $this->assertInstanceOf('Alchemy\Phrasea\Plugin\Schema\ManifestValidator', $validator); + } + + private function createValidator() + { + $schema = json_decode($this->getSchema()); + + return new ManifestValidator(new JsonSchemaValidator(), $schema); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php new file mode 100644 index 0000000000..2d9dfaa1be --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php @@ -0,0 +1,51 @@ +getSchema()); + + $validator = new PluginValidator(new ManifestValidator(new JsonValidator(), $schema)); + $validator->validatePlugin($directory); + } + + /** + * @dataProvider providePluginDirs + */ + public function testValidatePlugin($directory) + { + $schema = json_decode($this->getSchema()); + + $validator = new PluginValidator(new ManifestValidator(new JsonValidator(), $schema)); + $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'), + ); + } +} From ff7fd5401e76ced1517df31c94283f2a18151da9 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 13:08:17 +0200 Subject: [PATCH 05/13] Add plugin add and remove commands --- bin/developer | 2 +- .../Command/Plugin/AbstractPluginCommand.php | 41 +++++++++++++ .../Phrasea/Command/Plugin/AddPlugin.php | 61 +++++++++++++++++++ .../Phrasea/Command/Plugin/RemovePlugin.php | 43 +++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php create mode 100644 lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php create mode 100644 lib/Alchemy/Phrasea/Command/Plugin/RemovePlugin.php diff --git a/bin/developer b/bin/developer index 4ba8a54cc3..83213f9c72 100755 --- a/bin/developer +++ b/bin/developer @@ -34,7 +34,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__ . '/../plugins/autoload.php'; try { $cli = new CLI(" diff --git a/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php b/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php new file mode 100644 index 0000000000..e241c92d98 --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php @@ -0,0 +1,41 @@ +write("Validating plugins..."); + foreach ($this->container['plugins.explorer'] as $directory) { + $manifests[] = $manifest = $this->container['plugins.plugins-validator']->validatePlugin($directory); + } + $output->writeln(" OK"); + + 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(" OK"); + } +} diff --git a/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php b/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php new file mode 100644 index 0000000000..1fd0c2c0cd --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php @@ -0,0 +1,61 @@ +setDescription('Install 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 $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['plugins.directory'] . 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("Removing temporary directory..."); + $this->container['filesystem']->remove($temporaryDir); + $output->writeln(" OK"); + + $this->updateConfigFiles($input, $output); + + return 0; + } +} diff --git a/lib/Alchemy/Phrasea/Command/Plugin/RemovePlugin.php b/lib/Alchemy/Phrasea/Command/Plugin/RemovePlugin.php new file mode 100644 index 0000000000..8d26f5e22d --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/Plugin/RemovePlugin.php @@ -0,0 +1,43 @@ +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 $name..."); + $this->container['filesystem']->remove($path); + $output->writeln(" OK"); + + $this->updateConfigFiles($input, $output); + + return 0; + } +} From 765b043fa8df3d407f64ce57cae3287fa14b5fb1 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 13:09:11 +0200 Subject: [PATCH 06/13] Update autoloader path --- bin/console | 7 ++++++- bin/developer | 2 +- lib/autoload.php | 3 +++ tests/bootstrap.php | 2 +- www/api.php | 2 +- www/index.php | 2 +- www/index_dev.php | 2 +- 7 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 lib/autoload.php diff --git a/bin/console b/bin/console index e8eb5cf321..48ac46beb3 100755 --- a/bin/console +++ b/bin/console @@ -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; diff --git a/bin/developer b/bin/developer index 83213f9c72..2e185d3269 100755 --- a/bin/developer +++ b/bin/developer @@ -34,7 +34,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__ . '/../plugins/autoload.php'; +require_once __DIR__ . '/../lib/autoload.php'; try { $cli = new CLI(" diff --git a/lib/autoload.php b/lib/autoload.php new file mode 100644 index 0000000000..b62f6f9c5c --- /dev/null +++ b/lib/autoload.php @@ -0,0 +1,3 @@ +add('Alchemy\\Tests', __DIR__); $loader->add('', __DIR__ . "/classes"); diff --git a/www/api.php b/www/api.php index a0159a2140..2f87fc6873 100644 --- a/www/api.php +++ b/www/api.php @@ -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'; diff --git a/www/index.php b/www/index.php index a8794dd050..e9c76071b0 100644 --- a/www/index.php +++ b/www/index.php @@ -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'; diff --git a/www/index_dev.php b/www/index_dev.php index d4064c8b54..54a8856d07 100644 --- a/www/index_dev.php +++ b/www/index_dev.php @@ -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'; From dbf4951a15e80854c5e938aa68745ecb0daf5795 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 13:15:44 +0200 Subject: [PATCH 07/13] Add dependencies for plugins --- composer.json | 3 + composer.lock | 158 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 129 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 9594c84fae..d831160c28 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 64ce9978ac..08b7c8e15f 100644 --- a/composer.lock +++ b/composer.lock @@ -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/" } From 90ef0b3730e37b51311845cd3bbc5d9236a9c70c Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 14:18:13 +0200 Subject: [PATCH 08/13] Add temporary filesystem service provider --- .../TemporaryFilesystemServiceProvider.php | 21 +++++++++++++++++++ ...TemporaryFilesystemServiceProviderTest.php | 16 ++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php create mode 100644 tests/Alchemy/Tests/Phrasea/Core/Provider/TemporaryFilesystemServiceProviderTest.php diff --git a/lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php new file mode 100644 index 0000000000..4003b056a9 --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php @@ -0,0 +1,21 @@ +share(function (Application $app) { + return new TemporaryFilesystem($app['filesystem']); + }); + } + + public function boot(Application $app) + { + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/TemporaryFilesystemServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/TemporaryFilesystemServiceProviderTest.php new file mode 100644 index 0000000000..89b39e1c47 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/TemporaryFilesystemServiceProviderTest.php @@ -0,0 +1,16 @@ + Date: Thu, 30 May 2013 14:19:30 +0200 Subject: [PATCH 09/13] Register temporary filesystem and plugin service providers --- lib/Alchemy/Phrasea/Application.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 8d29579711..2672f79248 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -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); } /** From 44a98810b1b82714c224347df62fa4a5d6dfb583 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 17:52:02 +0200 Subject: [PATCH 10/13] Add plugin commands unit tests --- .../Phrasea/Command/Plugin/AddPlugin.php | 2 +- .../Core/Provider/PluginServiceProvider.php | 4 +- .../Phrasea/Command/Plugin/AddPluginTest.php | 76 +++++++++++++++++++ .../Command/Plugin/PluginCommandTestCase.php | 62 +++++++++++++++ .../Command/Plugin/RemovePluginTest.php | 33 ++++++++ 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 tests/Alchemy/Tests/Phrasea/Command/Plugin/AddPluginTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Command/Plugin/PluginCommandTestCase.php create mode 100644 tests/Alchemy/Tests/Phrasea/Command/Plugin/RemovePluginTest.php diff --git a/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php b/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php index 1fd0c2c0cd..ceaa403933 100644 --- a/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php +++ b/lib/Alchemy/Phrasea/Command/Plugin/AddPlugin.php @@ -22,7 +22,7 @@ class AddPlugin extends AbstractPluginCommand parent::__construct('plugins:add'); $this - ->setDescription('Install a plugin to Phraseanet') + ->setDescription('Installs a plugin to Phraseanet') ->addArgument('source', InputArgument::REQUIRED, 'The source is a folder'); } diff --git a/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php index 6207f94ab4..ed66db7d4e 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php @@ -29,8 +29,8 @@ class PluginServiceProvider implements ServiceProviderInterface { public function register(Application $app) { - $app['plugins.directory'] = __DIR__ . '/../../../../../plugins'; - $app['plugins.schema'] = __DIR__ . '/../../../../conf.d/plugin-schema.json'; + $app['plugins.directory'] = realpath(__DIR__ . '/../../../../../plugins'); + $app['plugins.schema'] = realpath(__DIR__ . '/../../../../conf.d/plugin-schema.json'); $app['json-validator'] = $app->share(function (Application $app) { return new JsonValidator(); diff --git a/tests/Alchemy/Tests/Phrasea/Command/Plugin/AddPluginTest.php b/tests/Alchemy/Tests/Phrasea/Command/Plugin/AddPluginTest.php new file mode 100644 index 0000000000..f5949dc939 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Command/Plugin/AddPluginTest.php @@ -0,0 +1,76 @@ +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); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Command/Plugin/PluginCommandTestCase.php b/tests/Alchemy/Tests/Phrasea/Command/Plugin/PluginCommandTestCase.php new file mode 100644 index 0000000000..d0affce512 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Command/Plugin/PluginCommandTestCase.php @@ -0,0 +1,62 @@ +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(); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Command/Plugin/RemovePluginTest.php b/tests/Alchemy/Tests/Phrasea/Command/Plugin/RemovePluginTest.php new file mode 100644 index 0000000000..bf81a8ec74 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Command/Plugin/RemovePluginTest.php @@ -0,0 +1,33 @@ +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); + } +} From 0d8f1aa79ef59ba67de48b095d202e284fc0ae78 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 30 May 2013 19:52:20 +0200 Subject: [PATCH 11/13] Fix unit tests --- lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php index ed66db7d4e..aeb508df29 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php @@ -57,7 +57,8 @@ class PluginServiceProvider implements ServiceProviderInterface }); $app['plugins.composer-installer'] = $app->share(function (Application $app) { - if (null === $phpBinary = $app['phraseanet.registry']->get('php_binary')) { + $phpBinary = $app['phraseanet.registry']->get('php_binary'); + if (!is_executable($phpBinary)) { $finder = new ExecutableFinder(); $phpBinary = $finder->find('php'); } From bdcb98a3d574e6cf31da538e67024dd325e5ad7e Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Fri, 31 May 2013 10:15:36 +0200 Subject: [PATCH 12/13] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a151f62b45..ca1705783c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) From dddf316f295853d3a08820e379cda54c6cf76668 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Fri, 31 May 2013 10:16:27 +0200 Subject: [PATCH 13/13] Address github comments --- .../Core/Provider/PluginServiceProvider.php | 4 +- .../TemporaryFilesystemServiceProvider.php | 9 +++ .../Plugin/Management/AutoloaderGenerator.php | 57 ++++++++++++++----- .../Plugin/Schema/ManifestValidator.php | 52 ++++++++++++----- .../Phrasea/Plugin/Schema/PluginValidator.php | 16 +++++- .../Provider/PluginServiceProviderTest.php | 2 +- .../Fixtures/manifest-wrong5-min-version.json | 5 ++ .../Fixtures/manifest-wrong6-max-version.json | 5 ++ .../Management/AutoloaderGeneratorTest.php | 2 +- .../Tests/Phrasea/Plugin/PluginTestCase.php | 9 ++- .../Plugin/Schema/ManifestValidatorTest.php | 8 ++- .../Plugin/Schema/PluginValidatorTest.php | 8 +-- 12 files changed, 131 insertions(+), 46 deletions(-) create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong5-min-version.json create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong6-max-version.json diff --git a/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php index aeb508df29..9c77ef5729 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/PluginServiceProvider.php @@ -32,12 +32,12 @@ class PluginServiceProvider implements ServiceProviderInterface $app['plugins.directory'] = realpath(__DIR__ . '/../../../../../plugins'); $app['plugins.schema'] = realpath(__DIR__ . '/../../../../conf.d/plugin-schema.json'); - $app['json-validator'] = $app->share(function (Application $app) { + $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['json-validator'], $app['plugins.schema']); + return ManifestValidator::create($app); }); $app['plugins.plugins-validator'] = $app->share(function (Application $app) { diff --git a/lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php index 4003b056a9..33ebf9d38a 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/TemporaryFilesystemServiceProvider.php @@ -1,5 +1,14 @@ getName() . DIRECTORY_SEPARATOR . "vendor" . DIRECTORY_SEPARATOR . "autoload.php';\n"; + $autoloader = '/' . $manifest->getName() . DIRECTORY_SEPARATOR . "vendor" . DIRECTORY_SEPARATOR . "autoload.php"; + $buffer .= <<getServices() as $service) { - $buffer .= " \$app->register(\\".$service['class']."::create(\$app));\n"; + $class = $service['class']; + $buffer .= <<register($class::create(\$app)); +EOF; } } - $buffer .= "\n return \$app;\n}, \$app);\n"; + $buffer .= <<validator = $validator; + $this->version = $version; $this->schemaData = $schemaData; } @@ -36,12 +40,12 @@ class ManifestValidator throw new InvalidArgumentException('Json Schema must be an object'); } - $validator = clone $this->validator; - $validator->check($data, $this->schemaData); + $this->validator->reset(); + $this->validator->check($data, $this->schemaData); - if (!$validator->isValid()) { + if (!$this->validator->isValid()) { $errors = array(); - foreach ((array) $validator->getErrors() as $error) { + 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); @@ -51,17 +55,35 @@ class ManifestValidator throw new JsonValidationException('Does not match the expected JSON schema', array('"name" must not contains only alphanumeric caracters')); } - // validate gainst versions - } - - public static function create(JsonValidator $jsonValidator, $path) - { - $data = @json_decode(@file_get_contents($path)); - - if (JSON_ERROR_NONE !== json_last_error()) { - throw new InvalidArgumentException(sprintf('Unable to read %s', $path)); + 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() + )); + } } - return new static($jsonValidator, $data); + 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']); } } diff --git a/lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php b/lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php index f28445c69c..0b0cadbdde 100644 --- a/lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php +++ b/lib/Alchemy/Phrasea/Plugin/Schema/PluginValidator.php @@ -43,8 +43,7 @@ class PluginValidator throw new PluginValidationException('Manifest file is invalid', $e->getCode(), $e); } - // a ameliorer - return new Manifest(@json_decode(@file_get_contents($manifest), true)); + return new Manifest($this->objectToArray($data)); } private function ensureManifest($directory) @@ -65,4 +64,17 @@ class PluginValidator 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; + } } diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php index 076f29fc00..df94de41b7 100644 --- a/tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/PluginServiceProviderTest.php @@ -16,7 +16,7 @@ class PluginServiceProvidertest extends ServiceProviderTestCase return array( array( 'Alchemy\Phrasea\Core\Provider\PluginServiceProvider', - 'json-validator', + 'plugins.json-validator', 'JsonSchema\Validator' ), array( diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong5-min-version.json b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong5-min-version.json new file mode 100644 index 0000000000..c63b1f8eda --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong5-min-version.json @@ -0,0 +1,5 @@ +{ + "name": "TestPlugin", + "description" : "A custom class connector", + "minimum-phraseanet-version": "14" +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong6-max-version.json b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong6-max-version.json new file mode 100644 index 0000000000..c8dbb299f4 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/manifest-wrong6-max-version.json @@ -0,0 +1,5 @@ +{ + "name": "TestPlugin", + "description" : "A custom class connector", + "maximum-phraseanet-version": "3.8" +} diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php index 8ee8241189..267cba71dd 100644 --- a/tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Management/AutoloaderGeneratorTest.php @@ -33,7 +33,7 @@ class AutoloaderGeneratorTest extends \PHPUnit_Framework_TestCase $this->assertFileExists($file); $process = ProcessBuilder::create(array($php, '-l', $file))->getProcess(); $process->run(); - $this->assertTrue($process->isSuccessful()); + $this->assertTrue($process->isSuccessful(), basename($file) . ' is valid'); } // test autoload diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php b/tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php index 8603550843..e1cd027f17 100644 --- a/tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php +++ b/tests/Alchemy/Tests/Phrasea/Plugin/PluginTestCase.php @@ -2,8 +2,15 @@ namespace Alchemy\Tests\Phrasea\Plugin; -class PluginTestCase extends \PHPUnit_Framework_TestCase +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'; diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php index dae749933a..5fb5a1bc99 100644 --- a/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/ManifestValidatorTest.php @@ -42,6 +42,8 @@ class ManifestValidatorTest extends PluginTestCase 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'), ); } @@ -59,12 +61,12 @@ class ManifestValidatorTest extends PluginTestCase */ public function testConstructWithInvalidSchema() { - $validator = new ManifestValidator(new JsonSchemaValidator(), array()); + new ManifestValidator(new JsonSchemaValidator(), array(), self::$DI['app']['phraseanet.version']); } public function testCreate() { - $validator = ManifestValidator::create(new JsonSchemaValidator(), $this->getSchemaPath()); + $validator = ManifestValidator::create(self::$DI['app']); $this->assertInstanceOf('Alchemy\Phrasea\Plugin\Schema\ManifestValidator', $validator); } @@ -73,6 +75,6 @@ class ManifestValidatorTest extends PluginTestCase { $schema = json_decode($this->getSchema()); - return new ManifestValidator(new JsonSchemaValidator(), $schema); + return new ManifestValidator(new JsonSchemaValidator(), $schema, self::$DI['app']['phraseanet.version']); } } diff --git a/tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php index 2d9dfaa1be..b477faa68f 100644 --- a/tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php +++ b/tests/Alchemy/Tests/Phrasea/Plugin/Schema/PluginValidatorTest.php @@ -15,9 +15,7 @@ class PluginValidatorTest extends PluginTestCase */ public function testValidateInvalidPlugin($directory) { - $schema = json_decode($this->getSchema()); - - $validator = new PluginValidator(new ManifestValidator(new JsonValidator(), $schema)); + $validator = new PluginValidator($this->createManifestValidator()); $validator->validatePlugin($directory); } @@ -26,9 +24,7 @@ class PluginValidatorTest extends PluginTestCase */ public function testValidatePlugin($directory) { - $schema = json_decode($this->getSchema()); - - $validator = new PluginValidator(new ManifestValidator(new JsonValidator(), $schema)); + $validator = new PluginValidator($this->createManifestValidator()); $validator->validatePlugin($directory); }