Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Extension Dependency Support #2188

Merged
merged 32 commits into from
Oct 2, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c8cfdad
Add relevant exceptions for enabling/disabling
askvortsov1 May 28, 2020
e50f22b
Add helper method for getting flarum extension dependencies
askvortsov1 May 28, 2020
7937f82
Add extension dependency resolver (Kahn's algorithm), plus unit tests
askvortsov1 May 28, 2020
acc4f65
Enable extensions in proper order
askvortsov1 May 28, 2020
ec3cd0c
Fix no default when getting extension dependencies
askvortsov1 May 28, 2020
0722382
Apply fixes from StyleCI
luceos May 28, 2020
6c686bb
Use require instead of dedicated field to get extension dependencies
askvortsov1 Jul 24, 2020
787515d
Apply fixes from StyleCI
luceos Jul 24, 2020
c374baf
Fix fake extension to not fail tests and be compliant
askvortsov1 Jul 24, 2020
30ee917
Remove unnecessary import
askvortsov1 Sep 6, 2020
fec6ff3
Fix constructor, remove KnownError since we'll need a custom handler
askvortsov1 Sep 6, 2020
54a35b4
Add custom handlers for dependency exceptions
askvortsov1 Sep 6, 2020
2e4ebef
Standardize return format
askvortsov1 Sep 6, 2020
d12fe5b
Add frontend error handler for admin dashboard
askvortsov1 Sep 6, 2020
6e4b867
Fix dependentExtensions saving the wrong thing
askvortsov1 Sep 6, 2020
7b405f0
Update src/Extension/Exception/DependentExtensionsException.php
askvortsov1 Sep 6, 2020
39ca931
Update for new alerts signature
askvortsov1 Sep 24, 2020
7be3c1f
Add more tests for dependency resolution
askvortsov1 Sep 24, 2020
d32bafc
Remove dependency resolution portion
askvortsov1 Sep 26, 2020
8becd7c
Store a set of installed extension composer names instead of an assoc…
askvortsov1 Sep 26, 2020
ebe0107
Improve explanation for installedSet, put its values as true, not null
askvortsov1 Sep 30, 2020
3fa2cdd
Fix comments
askvortsov1 Oct 2, 2020
e1d6378
Get rid of the $installedSet mess, use $extensions to check what is/i…
askvortsov1 Oct 2, 2020
7acf52b
Go back to using $installedSet to store composer package names for in…
askvortsov1 Oct 2, 2020
58477b5
Code cleanup
askvortsov1 Oct 2, 2020
8923b0c
Rename `extensionDependencies` to `extensionDependencyIds` for clarity
askvortsov1 Oct 2, 2020
12e60d9
Apply fixes from StyleCI
luceos Oct 2, 2020
4ba8aba
Improve extension dependency error messages, even though they aren't …
askvortsov1 Oct 2, 2020
294a8bc
Apply fixes from StyleCI
luceos Oct 2, 2020
36f8fa0
Fix leftover `extensionDependencies`
askvortsov1 Oct 2, 2020
fef0d48
Add workaround to modal bug caused by bootstrap JS
askvortsov1 Oct 2, 2020
d65b767
Remove unnecessary changes
askvortsov1 Oct 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions js/src/admin/components/ExtensionsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -131,4 +132,18 @@ export default class ExtensionsPage extends Page {

app.modal.show(LoadingModal);
}

onerror(e) {
app.modal.close();

const error = JSON.parse(e.responseText).errors[0];

app.alerts.show(
clarkwinkelmann marked this conversation as resolved.
Show resolved Hide resolved
{ type: 'error' },
app.translator.trans(`core.lib.error.${error.code}_message`, {
extension: error.extension,
extensions: error.extensions.join(', '),
})
);
}
}
31 changes: 31 additions & 0 deletions src/Extension/Exception/DependentExtensionsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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;

class DependentExtensionsException extends Exception
{
public $extension;
public $dependent_extensions;

/**
* @param $extension: The extension we are attempting to activate.
* @param $dependent_extensions: Extension IDs of the 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(implode("\n", $dependent_extensions));
}
}
34 changes: 34 additions & 0 deletions src/Extension/Exception/DependentExtensionsExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -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
clarkwinkelmann marked this conversation as resolved.
Show resolved Hide resolved
{
return (new HandledError(
$e,
'dependent_extensions',
409
))->withDetails($this->errorDetails($e));
}

protected function errorDetails(DependentExtensionsException $e): array
{
return [
[
'extension' => $e->extension->getId(),
'extensions' => $e->dependent_extensions
]
];
}
}
31 changes: 31 additions & 0 deletions src/Extension/Exception/MissingDependenciesException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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;

class MissingDependenciesException extends Exception
{
public $extension;
public $missing_dependencies;

/**
* @param $extension: The extension we are attempting to activate.
* @param $missing_dependencies: Extension IDs of the missing flarum extension dependencies for this extension
*/
public function __construct(Extension $extension, array $missing_dependencies = null)
{
$this->extension = $extension;
$this->missing_dependencies = $missing_dependencies;

parent::__construct(implode("\n", $missing_dependencies));
}
}
34 changes: 34 additions & 0 deletions src/Extension/Exception/MissingDependenciesExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -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->missing_dependencies
]
];
}
}
49 changes: 40 additions & 9 deletions src/Extension/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;
Expand Down Expand Up @@ -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.
*
Expand All @@ -74,6 +83,13 @@ class Extension implements Arrayable
*/
protected $composerJson;

/**
* An array of extension ids for extension dependencies.
*
* @var string[]
*/
protected $dependencies;

/**
* Whether the extension is installed.
*
Expand Down Expand Up @@ -104,9 +120,7 @@ public function __construct($path, $composerJson)
*/
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)
Expand Down Expand Up @@ -182,6 +196,22 @@ public function setVersion($version)
return $this;
}

/**
* Get the list of flarum extensions that this extension depends on.
*
* @param array $lockJson: The json manifest of all installed php extensions
*/
public function calculateDependencies($lockJson)
{
$this->extensionDependencies = (new Collection(Arr::get($this->composerJson, 'require', [])))
->keys()
->filter(function ($key) use ($lockJson) {
return array_key_exists($key, $lockJson);
})->map(function ($key) {
return static::nameToId($key);
})->toArray();
}

/**
* @return string
*/
Expand Down Expand Up @@ -363,12 +393,13 @@ public function migrate(Migrator $migrator, $direction = 'up')
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->path,
'icon' => $this->getIcon(),
'hasAssets' => $this->hasAssets(),
'hasMigrations' => $this->hasMigrations(),
'extensionDependencies' => $this->extensionDependencies,
], $this->composerJson);
}
}
46 changes: 46 additions & 0 deletions src/Extension/ExtensionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,21 @@ public function getExtensions()
// Composer 2.0 changes the structure of the installed.json manifest
$installed = $installed['packages'] ?? $installed;

// We calculate and store a set of installed extension IDs (associative array with true keys)
// to speed up figuring what is and isn't a flarum extension later on.
// By only including flarum extensions, we know that anything present in this associative copy
// is a flarum extension, removing the need to check for it's type.
// We store the installed flarum extensions as keys, not values, of this array
// so that we have constant lookup 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');
Expand All @@ -101,6 +111,13 @@ public function getExtensions()

$extensions->put($extension->getId(), $extension);
}

luceos marked this conversation as resolved.
Show resolved Hide resolved
$installedSet = $extensions->pluck('name')->toArray();

foreach ($extensions as $extension) {
$extension->calculateDependencies($installedSet);
}

$this->extensions = $extensions->sortBy(function ($extension, $name) {
return $extension->composerJsonAttribute('extra.flarum-extension.title');
});
Expand Down Expand Up @@ -133,6 +150,18 @@ public function enable($name)

$extension = $this->getExtension($name);

$missingDependencies = [];
$enabled = $this->getEnabled();
foreach ($extension->extensionDependencies as $dependency) {
if (! in_array($dependency, $enabled)) {
$missingDependencies[] = $dependency;
}
}

if (! empty($missingDependencies)) {
throw new Exception\MissingDependenciesException($extension, $missingDependencies);
}

$this->dispatcher->dispatch(new Enabling($extension));

$enabled = $this->getEnabled();
Expand Down Expand Up @@ -165,6 +194,23 @@ public function disable($name)

$extension = $this->getExtension($name);

$dependentExtensions = [];

foreach ($this->getEnabledExtensions() as $possibleDependent) {
foreach ($possibleDependent->extensionDependencies as $dependency) {
// We check all enabled extensions. For each of them, if any depend on the extension
// we're disabling, we add it to the list and move onto the next one.
if ($dependency === $extension->getId()) {
$dependentExtensions[] = $possibleDependent->getId();
break;
}
}
}

if (! empty($dependentExtensions)) {
throw new Exception\DependentExtensionsException($extension, $dependentExtensions);
}

$this->dispatcher->dispatch(new Disabling($extension));

unset($enabled[$k]);
Expand Down
6 changes: 6 additions & 0 deletions src/Foundation/ErrorServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,6 +61,8 @@ public function register()
return [
IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class,
ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class,
DependentExtensionsException::class => DependentExtensionsExceptionHandler::class,
MissingDependenciesException::class => MissingDependenciesExceptionHandler::class,
];
});

Expand Down