blessing-skin-server/app/Services/PluginManager.php

538 lines
14 KiB
PHP
Raw Normal View History

2016-08-24 22:43:04 +08:00
<?php
namespace App\Services;
2018-11-21 23:32:32 +08:00
use Storage;
2019-08-11 18:00:00 +08:00
use Exception;
use App\Events;
2018-06-29 15:11:42 +08:00
use Composer\Semver\Semver;
2016-10-17 12:20:55 +08:00
use Illuminate\Support\Arr;
2019-08-11 18:00:00 +08:00
use Illuminate\Support\Str;
2018-11-21 23:32:32 +08:00
use Composer\Semver\Comparator;
2016-10-24 22:32:07 +08:00
use Illuminate\Support\Collection;
use Illuminate\Filesystem\Filesystem;
use App\Exceptions\PrettyPageException;
2016-10-24 22:32:07 +08:00
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
2016-10-17 12:20:55 +08:00
2016-08-24 22:43:04 +08:00
class PluginManager
{
2019-08-11 18:00:00 +08:00
/**
* @var bool
*/
protected $booted = false;
2016-10-24 22:32:07 +08:00
/**
* @var Application
*/
2016-10-17 12:20:55 +08:00
protected $app;
2016-10-24 22:32:07 +08:00
/**
2019-03-23 15:44:16 +08:00
* @var Option
2016-10-24 22:32:07 +08:00
*/
protected $option;
2016-10-17 12:20:55 +08:00
/**
* @var Dispatcher
*/
protected $dispatcher;
/**
* @var Filesystem
*/
protected $filesystem;
/**
* @var Collection|null
*/
protected $plugins;
/**
* @var Collection
*/
protected $enabled;
2016-10-17 12:20:55 +08:00
public function __construct(
Application $app,
2019-03-23 15:44:16 +08:00
Option $option,
2016-10-17 12:20:55 +08:00
Dispatcher $dispatcher,
Filesystem $filesystem
) {
$this->app = $app;
$this->option = $option;
2016-10-17 12:20:55 +08:00
$this->dispatcher = $dispatcher;
$this->filesystem = $filesystem;
}
2019-08-11 18:00:00 +08:00
/**
* Boot all enabled plugins.
*/
public function boot()
{
if ($this->booted) {
return;
}
$this->enabled = collect(json_decode($this->option->get('plugins_enabled', '[]'), true));
$plugins = collect();
collect($this->filesystem->directories($this->getPluginsDir()))
->filter(function ($directory) {
return $this->filesystem->exists($directory.DIRECTORY_SEPARATOR.'package.json');
})
->each(function ($directory) use (&$plugins) {
$manifest = json_decode(
$this->filesystem->get($directory.DIRECTORY_SEPARATOR.'package.json'),
true
);
$name = $manifest['name'];
if ($plugins->has($name)) {
throw new PrettyPageException(trans('errors.plugins.duplicate', [
'dir1' => $plugins->get($name)->getPath(),
'dir2' => $directory,
]), 5);
}
$plugins->put($name, new Plugin($directory, $manifest));
});
// disable unsatisfied here
$this->registerAutoload($plugins->mapWithKeys(function ($plugin) {
return [$plugin->namespace => $plugin->getPath().'/src'];
}));
$this->booted = true;
}
/**
* @param Collection $paths
*/
protected function registerAutoload($paths)
{
spl_autoload_register(function ($class) use ($paths) {
$paths->each(function ($path, $namespace) use ($class) {
if ($namespace != '' && mb_strpos($class, $namespace) === 0) {
// Parse real file path
$path = $path.Str::replaceFirst($namespace, '', $class).'.php';
$path = str_replace('\\', '/', $path);
if ($this->filesystem->exists($path)) {
$this->filesystem->getRequire($path);
}
}
});
});
}
2016-10-17 12:20:55 +08:00
/**
* @return Collection
*/
public function getPlugins()
{
if (is_null($this->plugins)) {
$plugins = new Collection();
2018-11-21 23:32:32 +08:00
$enabled = $this->getFullEnabled();
2016-10-17 12:20:55 +08:00
$installed = [];
$cwd = getcwd();
chdir(base_path());
try {
$resource = opendir($this->getPluginsDir());
} catch (\Exception $e) {
throw new PrettyPageException(trans('errors.plugins.directory', ['msg' => $e->getMessage()]), 500);
}
2016-10-17 12:20:55 +08:00
// traverse plugins dir
while ($filename = @readdir($resource)) {
if ($filename == '.' || $filename == '..') {
2016-10-17 12:20:55 +08:00
continue;
}
2016-10-17 12:20:55 +08:00
$path = $this->getPluginsDir().DIRECTORY_SEPARATOR.$filename;
2016-10-17 12:20:55 +08:00
if (is_dir($path)) {
$packageJsonPath = $path.DIRECTORY_SEPARATOR.'package.json';
if (file_exists($packageJsonPath)) {
2016-10-17 12:20:55 +08:00
// load packages installed
$installed[$filename] = json_decode($this->filesystem->get($packageJsonPath), true);
2016-10-17 12:20:55 +08:00
}
}
}
closedir($resource);
foreach ($installed as $dirname => $package) {
2016-10-17 12:20:55 +08:00
// Instantiates an Plugin object using the package path and package.json file.
$plugin = new Plugin($this->getPluginsDir().DIRECTORY_SEPARATOR.$dirname, $package);
2016-10-17 12:20:55 +08:00
2019-03-21 19:45:52 +08:00
// Each default all plugins are installed if they are registered in composer.
$plugin->setDirname($dirname);
2016-10-17 12:20:55 +08:00
$plugin->setInstalled(true);
2016-10-24 22:32:07 +08:00
$plugin->setNameSpace(Arr::get($package, 'namespace'));
2016-10-17 12:20:55 +08:00
$plugin->setVersion(Arr::get($package, 'version'));
$plugin->setEnabled($this->isEnabled($plugin->name));
if ($plugins->has($plugin->name)) {
throw new PrettyPageException(trans('errors.plugins.duplicate', [
'dir1' => $plugin->getDirname(),
'dir2' => $plugins->get($plugin->name)->getDirname(),
]), 5);
}
2016-10-17 12:20:55 +08:00
$plugins->put($plugin->name, $plugin);
2018-11-21 23:32:32 +08:00
if (
$enabled->has($plugin->name) &&
Comparator::notEqualTo($plugin->getVersion(), $enabled->get($plugin->name))
) {
$this->copyPluginAssets($plugin);
}
2016-10-17 12:20:55 +08:00
}
$this->plugins = $plugins->sortBy(function ($plugin, $name) {
return $plugin->name;
});
chdir($cwd);
2016-10-17 12:20:55 +08:00
}
return $this->plugins;
}
/**
* Loads an Plugin with all information.
*
* @param string $name
* @return Plugin|null
*/
public function getPlugin($name)
2016-08-24 22:43:04 +08:00
{
2016-10-17 12:20:55 +08:00
return $this->getPlugins()->get($name);
}
/**
* Enables the plugin.
*
* @param string $name
*/
public function enable($name)
{
if (is_null($this->enabled)) {
$this->convertPluginRecord();
}
2016-10-17 12:20:55 +08:00
if (! $this->isEnabled($name)) {
$plugin = $this->getPlugin($name);
$this->enabled->push([
'name' => $name,
'version' => $plugin->getVersion(),
]);
$this->saveEnabled();
2016-10-17 12:20:55 +08:00
$plugin->setEnabled(true);
2019-03-21 19:45:52 +08:00
$this->copyPluginAssets($plugin);
2019-02-27 23:44:50 +08:00
$this->dispatcher->dispatch(new Events\PluginWasEnabled($plugin));
2016-10-17 12:20:55 +08:00
}
2016-08-24 22:43:04 +08:00
}
2016-10-17 12:20:55 +08:00
/**
* Disables an plugin.
*
* @param string $name
*/
public function disable($name)
{
if (is_null($this->enabled)) {
$this->convertPluginRecord();
}
2016-10-17 12:20:55 +08:00
$rejected = $this->enabled->reject(function ($item) use ($name) {
2019-04-22 09:52:45 +08:00
return is_string($item) ? $item == $name : $item['name'] == $name;
});
2016-10-17 12:20:55 +08:00
if ($rejected->count() !== $this->enabled->count()) {
2016-10-17 12:20:55 +08:00
$plugin = $this->getPlugin($name);
$plugin->setEnabled(false);
$this->enabled = $rejected;
$this->saveEnabled();
2019-02-27 23:44:50 +08:00
$this->dispatcher->dispatch(new Events\PluginWasDisabled($plugin));
2016-10-17 12:20:55 +08:00
}
}
/**
* Uninstalls an plugin.
*
* @param string $name
*/
public function uninstall($name)
{
$plugin = $this->getPlugin($name);
$this->disable($name);
2019-02-27 23:44:50 +08:00
// dispatch event before deleting plugin files
$this->dispatcher->dispatch(new Events\PluginWasDeleted($plugin));
2017-01-17 21:41:20 +08:00
2016-10-17 12:20:55 +08:00
$this->filesystem->deleteDirectory($plugin->getPath());
// refresh plugin list
$this->plugins = null;
}
/**
* Get only enabled plugins.
*
* @return Collection
*/
public function getEnabledPlugins()
{
if (is_null($this->enabled)) {
$this->convertPluginRecord();
}
2016-10-17 12:20:55 +08:00
return $this->getPlugins()->only($this->getEnabled());
}
/**
* Loads all bootstrap.php files of the enabled plugins.
*
* @return Collection
*/
public function getEnabledBootstrappers()
{
$bootstrappers = new Collection;
foreach ($this->getEnabledPlugins() as $plugin) {
if ($this->filesystem->exists($file = $plugin->getPath().'/bootstrap.php')) {
$bootstrappers->push($file);
}
}
return $bootstrappers;
}
2018-06-28 23:32:27 +08:00
/**
* Loads composer autoloader for the enabled plugins if exists.
*
* @return Collection
*/
public function getEnabledComposerAutoloaders()
{
$autoloaders = new Collection;
foreach ($this->getEnabledPlugins() as $plugin) {
if ($this->filesystem->exists($file = $plugin->getPath().'/vendor/autoload.php')) {
$autoloaders->push($file);
}
}
return $autoloaders;
}
2016-10-17 12:20:55 +08:00
/**
* The id's of the enabled plugins.
*
* @return array
*/
public function getEnabled()
{
$enabled = collect(json_decode($this->option->get('plugins_enabled'), true));
return $enabled->map(function ($item) {
if (is_string($item)) {
return $item;
} else {
return $item['name'];
}
})->values()->toArray();
2016-10-17 12:20:55 +08:00
}
2018-11-21 23:32:32 +08:00
/**
* Return enabled plugins with version information.
*
* @return Collection
*/
public function getFullEnabled()
{
$enabled = collect(json_decode($this->option->get('plugins_enabled'), true));
$ret = collect();
$enabled->each(function ($item) use ($ret) {
if (is_array($item)) {
$ret->put($item['name'], $item['version']);
}
});
return $ret;
}
2016-10-17 12:20:55 +08:00
/**
* Persist the currently enabled plugins.
*/
protected function saveEnabled()
2016-10-17 12:20:55 +08:00
{
$this->option->set('plugins_enabled', $this->enabled->values()->toJson());
2016-10-17 12:20:55 +08:00
}
/**
* Whether the plugin is enabled.
*
2018-06-29 15:11:42 +08:00
* @param string $pluginName
2016-10-17 12:20:55 +08:00
* @return bool
*/
2018-06-29 15:11:42 +08:00
public function isEnabled($pluginName)
2016-10-17 12:20:55 +08:00
{
2018-06-29 15:11:42 +08:00
return in_array($pluginName, $this->getEnabled());
}
/**
* Get the unsatisfied requirements of plugin.
*
2018-08-19 11:39:14 +08:00
* @param string|Plugin|array $plugin
2018-06-29 15:11:42 +08:00
* @return array
*/
public function getUnsatisfiedRequirements($plugin)
2018-06-29 15:11:42 +08:00
{
2018-08-19 11:39:14 +08:00
if (is_array($plugin)) {
$requirements = $plugin;
} else {
if (! $plugin instanceof Plugin) {
$plugin = $this->getPlugin($plugin);
}
2018-06-29 15:11:42 +08:00
2018-08-19 11:39:14 +08:00
if (! $plugin) {
throw new \InvalidArgumentException('Plugin with given name does not exist.');
}
2018-06-29 15:11:42 +08:00
2018-08-19 11:39:14 +08:00
$requirements = $plugin->getRequirements();
}
2018-06-29 15:11:42 +08:00
$unsatisfied = [];
foreach ($requirements as $name => $versionConstraint) {
// Version requirement for the main application
if ($name == 'blessing-skin-server') {
if (! Semver::satisfies(config('app.version'), $versionConstraint)) {
$unsatisfied['blessing-skin-server'] = [
'version' => config('app.version'),
'constraint' => $versionConstraint,
2018-06-29 15:11:42 +08:00
];
}
continue;
}
$requiredPlugin = $this->getPlugin($name);
if (! $requiredPlugin || ! $requiredPlugin->isEnabled()) {
2018-06-29 15:11:42 +08:00
$unsatisfied[$name] = [
'version' => null,
'constraint' => $versionConstraint,
2018-06-29 15:11:42 +08:00
];
continue;
}
if (! Semver::satisfies($requiredPlugin->getVersion(), $versionConstraint)) {
$unsatisfied[$name] = [
'version' => $requiredPlugin->getVersion(),
'constraint' => $versionConstraint,
2018-06-29 15:11:42 +08:00
];
continue;
}
}
return $unsatisfied;
}
/**
* Whether the plugin's requirements are satisfied.
*
2018-08-19 11:39:14 +08:00
* @param string|Plugin|array $plugin
2018-06-29 15:11:42 +08:00
* @return bool
*/
public function isRequirementsSatisfied($plugin)
2018-06-29 15:11:42 +08:00
{
return empty($this->getUnsatisfiedRequirements($plugin));
2016-10-17 12:20:55 +08:00
}
/**
* The plugins path.
*
* @return string
*/
2018-08-19 11:39:14 +08:00
public function getPluginsDir()
2016-10-17 12:20:55 +08:00
{
return config('plugins.directory') ? realpath(config('plugins.directory')) : base_path('plugins');
2016-10-17 12:20:55 +08:00
}
2018-10-19 22:47:01 +08:00
/**
* Copy plugin assets.
2018-10-19 22:47:01 +08:00
*
* @param Plugin $plugin
*
* @return bool
*/
public function copyPluginAssets($plugin)
{
2019-03-21 19:45:52 +08:00
$dir = public_path('plugins/'.$plugin->name);
2018-11-21 23:32:32 +08:00
Storage::deleteDirectory($dir);
2019-03-21 19:45:52 +08:00
$this->filesystem->copyDirectory(
$this->getPluginsDir().DIRECTORY_SEPARATOR.$plugin->name.DIRECTORY_SEPARATOR.'assets',
2019-03-21 19:45:52 +08:00
$dir.'/assets'
);
$this->filesystem->copyDirectory(
$this->getPluginsDir().DIRECTORY_SEPARATOR.$plugin->name.DIRECTORY_SEPARATOR.'lang',
$dir.'/lang'
2018-10-19 22:47:01 +08:00
);
}
/**
* Convert `plugins_enabled` field for backward compatibility.
*
* @return $this
*/
protected function convertPluginRecord()
{
$list = collect(json_decode($this->option->get('plugins_enabled'), true));
$this->enabled = $list->map(function ($item) {
if (is_string($item)) {
$plugin = $this->getPlugin($item);
// If we cannot read the package of that plugin, just return it as-is.
if (is_null($plugin)) {
return $item;
}
return [
'name' => $item,
'version' => $plugin->getVersion(),
];
} else {
2018-11-21 23:32:32 +08:00
$plugin = $this->getPlugin($item['name']);
if (! empty($plugin)) {
$item['version'] = $plugin->getVersion();
}
return $item;
}
});
$this->saveEnabled();
return $this;
}
2016-08-24 22:43:04 +08:00
}