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

452 lines
13 KiB
PHP
Raw Normal View History

2016-08-24 22:43:04 +08:00
<?php
namespace App\Services;
use App\Events;
use App\Exceptions\PrettyPageException;
2019-12-29 14:50:25 +08:00
use Composer\Autoload\ClassLoader;
2019-12-14 11:10:37 +08:00
use Composer\Semver\Comparator;
use Composer\Semver\Semver;
2016-10-24 22:32:07 +08:00
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
2019-12-14 11:10:37 +08:00
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
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;
2019-12-29 14:50:25 +08:00
/**
* @var ClassLoader
*/
protected $loader;
2016-10-17 12:20:55 +08:00
/**
* @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-12 17:37:52 +08:00
$this->enabled = collect();
2019-12-29 14:50:25 +08:00
$this->loader = new ClassLoader();
2016-10-17 12:20:55 +08:00
}
2019-08-11 18:00:00 +08:00
/**
2019-08-13 18:42:17 +08:00
* Get all installed plugins.
*
* @return Collection
2019-08-11 18:00:00 +08:00
*/
2019-08-13 18:42:17 +08:00
public function all()
2019-08-11 18:00:00 +08:00
{
2019-08-13 18:42:17 +08:00
if (filled($this->plugins)) {
return $this->plugins;
2019-08-11 18:00:00 +08:00
}
2019-08-13 18:42:17 +08:00
$this->enabled = collect(json_decode($this->option->get('plugins_enabled', '[]'), true))
2019-08-16 14:46:55 +08:00
->reject(function ($item) {
return is_string($item);
})
2019-08-13 18:42:17 +08:00
->mapWithKeys(function ($item) {
return [$item['name'] => ['version' => $item['version']]];
});
2019-08-11 18:00:00 +08:00
$plugins = collect();
2019-09-09 23:08:03 +08:00
$versionChanged = [];
2019-08-11 18:00:00 +08:00
2019-08-19 23:06:17 +08:00
$this->getPluginsDirs()
->flatMap(function ($directory) {
return $this->filesystem->directories($directory);
})
->unique()
2019-08-11 18:00:00 +08:00
->filter(function ($directory) {
return $this->filesystem->exists($directory.DIRECTORY_SEPARATOR.'package.json');
})
2019-09-09 23:08:03 +08:00
->each(function ($directory) use (&$plugins, &$versionChanged) {
2019-08-11 18:00:00 +08:00
$manifest = json_decode(
$this->filesystem->get($directory.DIRECTORY_SEPARATOR.'package.json'),
true
);
$name = $manifest['name'];
if ($plugins->has($name)) {
2019-12-14 11:10:37 +08:00
throw new PrettyPageException(trans('errors.plugins.duplicate', ['dir1' => $plugins->get($name)->getPath(), 'dir2' => $directory]), 5);
2019-08-11 18:00:00 +08:00
}
2019-08-12 10:52:40 +08:00
$plugin = new Plugin($directory, $manifest);
2019-08-12 17:37:52 +08:00
$plugins->put($name, $plugin);
2019-08-21 17:31:51 +08:00
if ($this->getUnsatisfied($plugin)->isNotEmpty() || $this->getConflicts($plugin)->isNotEmpty()) {
2019-08-12 17:37:52 +08:00
$this->disable($plugin);
}
2019-08-13 18:42:17 +08:00
if ($this->enabled->has($name)) {
2019-08-12 10:52:40 +08:00
$plugin->setEnabled(true);
2019-08-12 15:21:50 +08:00
if (Comparator::notEqualTo(
$manifest['version'],
2019-08-13 18:42:17 +08:00
$this->enabled->get($name)['version']
2019-08-12 15:21:50 +08:00
)) {
2019-08-28 15:32:49 +08:00
$this->enabled->put($name, ['version' => $manifest['version']]);
2019-09-09 23:08:03 +08:00
$versionChanged[] = $plugin;
2019-08-12 15:21:50 +08:00
}
2019-08-12 10:52:40 +08:00
}
2019-08-11 18:00:00 +08:00
});
2019-08-13 18:42:17 +08:00
$this->plugins = $plugins;
2019-09-09 23:08:03 +08:00
if (count($versionChanged) > 0) {
$this->saveEnabled();
}
array_walk($versionChanged, function ($plugin) {
$this->dispatcher->dispatch('plugin.versionChanged', [$plugin]);
});
2019-08-13 18:42:17 +08:00
return $plugins;
}
/**
* Boot all enabled plugins.
*/
public function boot()
{
if ($this->booted) {
return;
}
$this->all()->each(function ($plugin) {
$this->loadViewsAndTranslations($plugin);
});
2019-08-19 16:52:10 +08:00
$enabled = $this->getEnabledPlugins();
$enabled->each(function ($plugin) {
$this->registerPlugin($plugin);
});
2019-12-29 14:50:25 +08:00
$this->loader->register();
2019-08-19 16:52:10 +08:00
$enabled->each(function ($plugin) {
2019-08-16 15:51:20 +08:00
$this->bootPlugin($plugin);
});
2019-08-12 15:59:01 +08:00
$this->registerLifecycleHooks();
2019-08-11 18:00:00 +08:00
$this->booted = true;
}
/**
2019-08-19 16:52:10 +08:00
* Register resources of a plugin.
2019-08-11 18:00:00 +08:00
*/
2019-08-19 16:52:10 +08:00
public function registerPlugin(Plugin $plugin)
2019-08-11 18:00:00 +08:00
{
2019-08-16 15:51:20 +08:00
$this->registerAutoload($plugin);
$this->loadVendor($plugin);
2019-08-19 16:52:10 +08:00
}
/**
* Boot a plugin.
*/
public function bootPlugin(Plugin $plugin)
{
2019-08-16 15:51:20 +08:00
$this->registerServiceProviders($plugin);
$this->loadBootstrapper($plugin);
}
2019-08-11 18:00:00 +08:00
2019-08-16 15:51:20 +08:00
/**
* Register classes autoloading.
*/
protected function registerAutoload(Plugin $plugin)
{
2019-12-29 14:50:25 +08:00
$this->loader->addPsr4(
Str::finish($plugin->namespace, '\\'),
$plugin->getPath().'/src'
);
2019-08-11 18:00:00 +08:00
}
2019-08-12 14:35:36 +08:00
/**
* Load Composer dumped autoload file.
*/
2019-08-16 15:51:20 +08:00
protected function loadVendor(Plugin $plugin)
2019-08-12 14:35:36 +08:00
{
2019-08-16 15:51:20 +08:00
$path = $plugin->getPath().'/vendor/autoload.php';
if ($this->filesystem->exists($path)) {
$this->filesystem->getRequire($path);
}
2019-08-12 14:35:36 +08:00
}
/**
* Load views and translations.
*/
2019-08-16 15:51:20 +08:00
protected function loadViewsAndTranslations(Plugin $plugin)
2019-08-12 14:35:36 +08:00
{
2019-08-16 15:51:20 +08:00
$namespace = $plugin->namespace;
$path = $plugin->getPath();
2019-08-12 14:35:36 +08:00
2019-09-06 18:52:34 +08:00
$translations = $this->app->make('translation.loader');
2019-08-16 15:51:20 +08:00
$translations->addNamespace($namespace, $path.'/lang');
2019-09-06 18:52:34 +08:00
$view = $this->app->make('view');
2019-08-16 15:51:20 +08:00
$view->addNamespace($namespace, $path.'/views');
2019-08-12 14:35:36 +08:00
}
2019-08-16 15:51:20 +08:00
protected function registerServiceProviders(Plugin $plugin)
2019-08-16 14:56:47 +08:00
{
2019-08-16 15:51:20 +08:00
$providers = Arr::get($plugin->getManifest(), 'enchants.providers', []);
array_walk($providers, function ($provider) use ($plugin) {
$class = Str::start(Str::finish($provider, 'ServiceProvider'), $plugin->namespace.'\\');
if (class_exists($class)) {
$this->app->register($class);
}
2019-08-16 14:56:47 +08:00
});
}
2019-08-12 14:35:36 +08:00
/**
* Load plugin's bootstrapper.
*/
2019-08-16 15:51:20 +08:00
protected function loadBootstrapper(Plugin $plugin)
2019-08-12 14:35:36 +08:00
{
2019-08-16 15:51:20 +08:00
$path = $plugin->getPath().'/bootstrap.php';
if ($this->filesystem->exists($path)) {
2019-08-31 09:12:51 +08:00
try {
$this->app->call($this->filesystem->getRequire($path), ['plugin' => $plugin]);
} catch (\Throwable $th) {
report($th);
if (is_a($th, \Exception::class)) {
$handler = $this->app->make(\App\Exceptions\Handler::class);
2019-12-14 11:10:37 +08:00
if (!$handler->shouldReport($th)) {
2019-08-31 09:12:51 +08:00
throw $th;
}
}
$this->dispatcher->dispatch(new Events\PluginBootFailed($plugin));
}
2019-08-16 15:51:20 +08:00
}
2019-08-12 14:35:36 +08:00
}
2019-08-12 15:59:01 +08:00
protected function registerLifecycleHooks()
{
$this->dispatcher->listen([
Events\PluginWasEnabled::class,
Events\PluginWasDisabled::class,
Events\PluginWasDeleted::class,
], function ($event) {
$plugin = $event->plugin;
$path = $plugin->getPath().'/callbacks.php';
if ($this->filesystem->exists($path)) {
$callbacks = $this->filesystem->getRequire($path);
$callback = Arr::get($callbacks, get_class($event));
if ($callback) {
return $this->app->call($callback, [$plugin]);
}
}
});
}
2019-08-13 18:42:17 +08:00
/**
* @return Plugin|null
*/
public function get(string $name)
{
return $this->all()->get($name);
}
/**
2019-12-14 11:10:37 +08:00
* @return bool|array return `true` if succeeded, or return information if failed
*/
2019-08-15 16:54:12 +08:00
public function enable($plugin)
2016-10-17 12:20:55 +08:00
{
2019-08-15 16:54:12 +08:00
$plugin = is_string($plugin) ? $this->get($plugin) : $plugin;
2019-12-14 11:10:37 +08:00
if ($plugin && !$plugin->isEnabled()) {
$unsatisfied = $this->getUnsatisfied($plugin);
$conflicts = $this->getConflicts($plugin);
if ($unsatisfied->isNotEmpty() || $conflicts->isNotEmpty()) {
return compact('unsatisfied', 'conflicts');
}
2019-08-15 16:54:12 +08:00
$this->enabled->put($plugin->name, ['version' => $plugin->version]);
$this->saveEnabled();
2016-10-17 12:20:55 +08:00
$plugin->setEnabled(true);
2019-02-27 23:44:50 +08:00
$this->dispatcher->dispatch(new Events\PluginWasEnabled($plugin));
return true;
} else {
return false;
2016-10-17 12:20:55 +08:00
}
2016-08-24 22:43:04 +08:00
}
2016-10-17 12:20:55 +08:00
2019-08-15 16:54:12 +08:00
public function disable($plugin)
2016-10-17 12:20:55 +08:00
{
2019-08-15 16:54:12 +08:00
$plugin = is_string($plugin) ? $this->get($plugin) : $plugin;
if ($plugin && $plugin->isEnabled()) {
$this->enabled->pull($plugin->name);
$this->saveEnabled();
2016-10-17 12:20:55 +08:00
$plugin->setEnabled(false);
2019-02-27 23:44:50 +08:00
$this->dispatcher->dispatch(new Events\PluginWasDisabled($plugin));
2016-10-17 12:20:55 +08:00
}
}
2019-08-15 16:54:12 +08:00
public function delete($plugin)
2016-10-17 12:20:55 +08:00
{
2019-08-15 16:54:12 +08:00
$plugin = is_string($plugin) ? $this->get($plugin) : $plugin;
if ($plugin) {
$this->disable($plugin);
2016-10-17 12:20:55 +08:00
2019-08-15 16:54:12 +08:00
// dispatch event before deleting plugin files
$this->dispatcher->dispatch(new Events\PluginWasDeleted($plugin));
2016-10-17 12:20:55 +08:00
2019-08-15 16:54:12 +08:00
$this->filesystem->deleteDirectory($plugin->getPath());
2017-01-17 21:41:20 +08:00
2019-08-15 16:54:12 +08:00
$this->plugins->pull($plugin->name);
}
2016-10-17 12:20:55 +08:00
}
/**
* @return Collection
*/
public function getEnabledPlugins()
{
2019-08-13 23:06:28 +08:00
return $this->all()->filter(function ($plugin) {
return $plugin->isEnabled();
2018-11-21 23:32:32 +08:00
});
}
2016-10-17 12:20:55 +08:00
/**
* Persist the currently enabled plugins.
*/
protected function saveEnabled()
2016-10-17 12:20:55 +08:00
{
2019-08-13 18:42:17 +08:00
$this->option->set('plugins_enabled', $this->enabled->map(function ($info, $name) {
2019-08-13 23:06:28 +08:00
return array_merge(compact('name'), $info);
})->values()->toJson());
2018-06-29 15:11:42 +08:00
}
2019-08-12 17:37:52 +08:00
/**
* @return Collection
*/
public function getUnsatisfied(Plugin $plugin)
{
return collect(Arr::get($plugin->getManifest(), 'require', []))
->mapWithKeys(function ($constraint, $name) {
if ($name == 'blessing-skin-server') {
$version = config('app.version');
2019-12-14 11:10:37 +08:00
return (!Semver::satisfies($version, $constraint))
2019-08-12 17:37:52 +08:00
? [$name => compact('version', 'constraint')]
: [];
} elseif ($name == 'php') {
preg_match('/(\d+\.\d+\.\d+)/', PHP_VERSION, $matches);
$version = $matches[1];
2019-12-14 11:10:37 +08:00
return (!Semver::satisfies($version, $constraint))
2019-08-12 17:37:52 +08:00
? [$name => compact('version', 'constraint')]
: [];
2019-12-14 11:10:37 +08:00
} elseif (!$this->enabled->has($name)) {
2019-08-12 17:37:52 +08:00
return [$name => ['version' => null, 'constraint' => $constraint]];
} else {
2019-08-13 18:42:17 +08:00
$version = $this->enabled->get($name)['version'];
2019-12-14 11:10:37 +08:00
return (!Semver::satisfies($version, $constraint))
2019-08-12 17:37:52 +08:00
? [$name => compact('version', 'constraint')]
: [];
}
});
}
/**
* @return Collection
*/
public function getConflicts(Plugin $plugin)
{
return collect($plugin->getManifestAttr('enchants.conflicts', []))
->mapWithKeys(function ($constraint, $name) {
$info = $this->enabled->get($name);
if ($info && Semver::satisfies($info['version'], $constraint)) {
return [$name => ['version' => $info['version'], 'constraint' => $constraint]];
} else {
return [];
}
});
}
/**
* Format the "unresolved" information into human-readable text.
*/
public function formatUnresolved(
Collection $unsatisfied,
Collection $conflicts
): array {
$unsatisfied = $unsatisfied->map(function ($detail, $name) {
$constraint = $detail['constraint'];
2019-12-14 11:10:37 +08:00
if (!$detail['version']) {
$plugin = $this->get($name);
$name = $plugin ? trans($plugin->title) : $name;
return trans('admin.plugins.operations.unsatisfied.disabled', compact('name'));
} else {
$title = trans($this->get($name)->title);
return trans('admin.plugins.operations.unsatisfied.version', compact('title', 'constraint'));
}
})->values()->all();
$conflicts = $conflicts->map(function ($detail, $name) {
$title = trans($this->get($name)->title);
return trans('admin.plugins.operations.unsatisfied.conflict', compact('title'));
})->values()->all();
return array_merge($unsatisfied, $conflicts);
}
2016-10-17 12:20:55 +08:00
/**
* The plugins path.
*
2019-08-19 23:06:17 +08:00
* @return Collection
2016-10-17 12:20:55 +08:00
*/
2019-08-19 23:06:17 +08:00
public function getPluginsDirs()
2016-10-17 12:20:55 +08:00
{
2019-08-19 23:06:17 +08:00
$config = config('plugins.directory');
if ($config) {
return collect(preg_split('/,\s*/', $config))
->map(function ($directory) {
return realpath($directory) ?: $directory;
});
} else {
return collect([base_path('plugins')]);
}
2016-10-17 12:20:55 +08:00
}
2016-08-24 22:43:04 +08:00
}