Basic Extension Dependency Support (#2188)

- Don't enable an extension if its dependencies are not enabled
- Don't disable an extension if its dependencies are not disabled
This commit is contained in:
Alexander Skvortsov 2020-10-02 17:54:28 -04:00 committed by GitHub
parent 9251aa925f
commit eb717bb034
8 changed files with 279 additions and 13 deletions

View File

@ -123,6 +123,7 @@ export default class ExtensionsPage extends Page {
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
method: 'PATCH',
body: { enabled: !enabled },
errorHandler: this.onerror.bind(this),
})
.then(() => {
if (!enabled) localStorage.setItem('enabledExtension', id);
@ -131,4 +132,23 @@ export default class ExtensionsPage extends Page {
app.modal.show(LoadingModal);
}
onerror(e) {
// We need to give the modal animation time to start; if we close the modal too early,
// it breaks the bootstrap modal library.
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
setTimeout(() => {
app.modal.close();
const error = JSON.parse(e.responseText).errors[0];
app.alerts.show(
{ type: 'error' },
app.translator.trans(`core.lib.error.${error.code}_message`, {
extension: error.extension,
extensions: error.extensions.join(', '),
})
);
}, 250);
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extension\Exception;
use Exception;
use Flarum\Extension\Extension;
/**
* This exception is thrown when someone attempts to disable an extension
* that other enabled extensions depend on.
*/
class DependentExtensionsException extends Exception
{
public $extension;
public $dependent_extensions;
/**
* @param $extension: The extension we are attempting to disable.
* @param $dependent_extensions: Enabled Flarum extensions that depend on this extension.
*/
public function __construct(Extension $extension, array $dependent_extensions)
{
$this->extension = $extension;
$this->dependent_extensions = $dependent_extensions;
parent::__construct($extension->getId().' could not be disabled, because it is a dependency of: '.implode(', ', $this->getDependentExtensionIds()));
}
/**
* Get array of IDs for extensions that depend on this extension.
*
* @return array
*/
public function getDependentExtensionIds()
{
return array_map(function (Extension $extension) {
return $extension->getId();
}, $this->dependent_extensions);
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extension\Exception;
use Flarum\Foundation\ErrorHandling\HandledError;
class DependentExtensionsExceptionHandler
{
public function handle(DependentExtensionsException $e): HandledError
{
return (new HandledError(
$e,
'dependent_extensions',
409
))->withDetails($this->errorDetails($e));
}
protected function errorDetails(DependentExtensionsException $e): array
{
return [
[
'extension' => $e->extension->getId(),
'extensions' => $e->getDependentExtensionIds(),
]
];
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extension\Exception;
use Exception;
use Flarum\Extension\Extension;
/**
* This exception is thrown when someone attempts to enable an extension
* whose Flarum extension dependencies are not all enabled.
*/
class MissingDependenciesException extends Exception
{
public $extension;
public $missing_dependencies;
/**
* @param $extension: The extension we are attempting to enable.
* @param $missing_dependencies: Extensions that this extension depends on, and are not enabled.
*/
public function __construct(Extension $extension, array $missing_dependencies = null)
{
$this->extension = $extension;
$this->missing_dependencies = $missing_dependencies;
parent::__construct($extension->getId().' could not be enabled, because it depends on: '.implode(', ', $this->getMissingDependencyIds()));
}
/**
* Get array of IDs for missing (disabled) extensions that this extension depends on.
*
* @return array
*/
public function getMissingDependencyIds()
{
return array_map(function (Extension $extension) {
return $extension->getId();
}, $this->missing_dependencies);
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extension\Exception;
use Flarum\Foundation\ErrorHandling\HandledError;
class MissingDependenciesExceptionHandler
{
public function handle(MissingDependenciesException $e): HandledError
{
return (new HandledError(
$e,
'missing_dependencies',
409
))->withDetails($this->errorDetails($e));
}
protected function errorDetails(MissingDependenciesException $e): array
{
return [
[
'extension' => $e->extension->getId(),
'extensions' => $e->getMissingDependencyIds(),
]
];
}
}

View File

@ -15,6 +15,7 @@ use Flarum\Extend\LifecycleInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
@ -51,6 +52,14 @@ class Extension implements Arrayable
'jpg' => 'image/jpeg',
];
protected static function nameToId($name)
{
list($vendor, $package) = explode('/', $name);
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
return "$vendor-$package";
}
/**
* Unique Id of the extension.
*
@ -60,6 +69,7 @@ class Extension implements Arrayable
* @var string
*/
protected $id;
/**
* The directory of this extension.
*
@ -74,6 +84,13 @@ class Extension implements Arrayable
*/
protected $composerJson;
/**
* The IDs of all Flarum extensions that this extension depends on.
*
* @var string[]
*/
protected $extensionDependencyIds;
/**
* Whether the extension is installed.
*
@ -104,9 +121,7 @@ class Extension implements Arrayable
*/
protected function assignId()
{
list($vendor, $package) = explode('/', $this->name);
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
$this->id = "$vendor-$package";
$this->id = static::nameToId($this->name);
}
public function extend(Container $app)
@ -182,6 +197,24 @@ class Extension implements Arrayable
return $this;
}
/**
* Get the list of flarum extensions that this extension depends on.
*
* @param array $extensionSet: An associative array where keys are the composer package names
* of installed extensions. Used to figure out which dependencies
* are flarum extensions.
*/
public function calculateDependencies($extensionSet)
{
$this->extensionDependencyIds = (new Collection(Arr::get($this->composerJson, 'require', [])))
->keys()
->filter(function ($key) use ($extensionSet) {
return array_key_exists($key, $extensionSet);
})->map(function ($key) {
return static::nameToId($key);
})->toArray();
}
/**
* @return string
*/
@ -253,6 +286,16 @@ class Extension implements Arrayable
return $this->path;
}
/**
* The IDs of all Flarum extensions that this extension depends on.
*
* @return array
*/
public function getExtensionDependencyIds()
{
return $this->extensionDependencyIds;
}
private function getExtenders(): array
{
$extenderFile = $this->getExtenderFile();
@ -363,12 +406,13 @@ class Extension implements Arrayable
public function toArray()
{
return (array) array_merge([
'id' => $this->getId(),
'version' => $this->getVersion(),
'path' => $this->path,
'icon' => $this->getIcon(),
'hasAssets' => $this->hasAssets(),
'hasMigrations' => $this->hasMigrations(),
'id' => $this->getId(),
'version' => $this->getVersion(),
'path' => $this->getPath(),
'icon' => $this->getIcon(),
'hasAssets' => $this->hasAssets(),
'hasMigrations' => $this->hasMigrations(),
'extensionDependencyIds' => $this->getExtensionDependencyIds(),
], $this->composerJson);
}
}

View File

@ -83,11 +83,18 @@ class ExtensionManager
// Composer 2.0 changes the structure of the installed.json manifest
$installed = $installed['packages'] ?? $installed;
// We calculate and store a set of composer package names for all installed Flarum extensions,
// so we know what is and isn't a flarum extension in `calculateDependencies`.
// Using keys of an associative array allows us to do these checks in constant time.
$installedSet = [];
foreach ($installed as $package) {
if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) {
continue;
}
$installedSet[Arr::get($package, 'name')] = true;
$path = isset($package['install-path'])
? $this->paths->vendor.'/composer/'.$package['install-path']
: $this->paths->vendor.'/'.Arr::get($package, 'name');
@ -101,6 +108,11 @@ class ExtensionManager
$extensions->put($extension->getId(), $extension);
}
foreach ($extensions as $extension) {
$extension->calculateDependencies($installedSet);
}
$this->extensions = $extensions->sortBy(function ($extension, $name) {
return $extension->composerJsonAttribute('extra.flarum-extension.title');
});
@ -133,17 +145,27 @@ class ExtensionManager
$extension = $this->getExtension($name);
$missingDependencies = [];
$enabledIds = $this->getEnabled();
foreach ($extension->getExtensionDependencyIds() as $dependencyId) {
if (! in_array($dependencyId, $enabledIds)) {
$missingDependencies[] = $this->getExtension($dependencyId);
}
}
if (! empty($missingDependencies)) {
throw new Exception\MissingDependenciesException($extension, $missingDependencies);
}
$this->dispatcher->dispatch(new Enabling($extension));
$enabled = $this->getEnabled();
$enabled[] = $name;
$enabledIds[] = $name;
$this->migrate($extension);
$this->publishAssets($extension);
$this->setEnabled($enabled);
$this->setEnabled($enabledIds);
$extension->enable($this->container);
@ -165,6 +187,18 @@ class ExtensionManager
$extension = $this->getExtension($name);
$dependentExtensions = [];
foreach ($this->getEnabledExtensions() as $possibleDependent) {
if (in_array($extension->getId(), $possibleDependent->getExtensionDependencyIds())) {
$dependentExtensions[] = $possibleDependent;
}
}
if (! empty($dependentExtensions)) {
throw new Exception\DependentExtensionsException($extension, $dependentExtensions);
}
$this->dispatcher->dispatch(new Disabling($extension));
unset($enabled[$k]);

View File

@ -9,6 +9,10 @@
namespace Flarum\Foundation;
use Flarum\Extension\Exception\DependentExtensionsException;
use Flarum\Extension\Exception\DependentExtensionsExceptionHandler;
use Flarum\Extension\Exception\MissingDependenciesException;
use Flarum\Extension\Exception\MissingDependenciesExceptionHandler;
use Flarum\Foundation\ErrorHandling\ExceptionHandler;
use Flarum\Foundation\ErrorHandling\LogReporter;
use Flarum\Foundation\ErrorHandling\Registry;
@ -57,6 +61,8 @@ class ErrorServiceProvider extends AbstractServiceProvider
return [
IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class,
ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class,
DependentExtensionsException::class => DependentExtensionsExceptionHandler::class,
MissingDependenciesException::class => MissingDependenciesExceptionHandler::class,
];
});