Implemented extensions as an object, usable by backend and frontend.

This commit is contained in:
Daniel Klabbers 2016-02-10 15:12:24 +01:00 committed by Franz Liedke
parent 10f3846646
commit b11b952aff
10 changed files with 430 additions and 117 deletions

View File

@ -11,7 +11,6 @@ import listItems from 'flarum/helpers/listItems';
export default class ExtensionsPage extends Component { export default class ExtensionsPage extends Component {
view() { view() {
const extensions = Object.keys(app.extensions).map(id => app.extensions[id]);
return ( return (
<div className="ExtensionsPage"> <div className="ExtensionsPage">
@ -29,15 +28,15 @@ export default class ExtensionsPage extends Component {
<div className="ExtensionsPage-list"> <div className="ExtensionsPage-list">
<div className="container"> <div className="container">
<ul className="ExtensionList"> <ul className="ExtensionList">
{extensions {Object.keys(app.extensions)
.sort((a, b) => a.extra['flarum-extension'].title.localeCompare(b.extra['flarum-extension'].title)) .map(id => {
.map(extension => { const extension = app.extensions[id];
const controls = this.controlItems(extension.id).toArray(); const controls = this.controlItems(extension.id).toArray();
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}> return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
<div className="ExtensionListItem-content"> <div className="ExtensionListItem-content">
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.extra['flarum-extension'].icon}> <span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.extra['flarum-extension'].icon ? icon(extension.extra['flarum-extension'].icon.name) : ''} {extension.icon ? icon(extension.icon.name) : ''}
</span> </span>
{controls.length ? ( {controls.length ? (
<Dropdown <Dropdown

View File

@ -72,7 +72,7 @@ class ClientController extends BaseClientController
$view->setVariable('settings', $settings); $view->setVariable('settings', $settings);
$view->setVariable('permissions', Permission::map()); $view->setVariable('permissions', Permission::map());
$view->setVariable('extensions', $this->extensions->getInfo()); $view->setVariable('extensions', $this->extensions->getExtensions()->toArray());
return $view; return $view;
} }

View File

@ -41,7 +41,6 @@ class DatabaseMigrationRepository implements MigrationRepositoryInterface
* *
* @param \Illuminate\Database\ConnectionResolverInterface $resolver * @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param string $table * @param string $table
* @return void
*/ */
public function __construct(Resolver $resolver, $table) public function __construct(Resolver $resolver, $table)
{ {

View File

@ -11,6 +11,7 @@
namespace Flarum\Database; namespace Flarum\Database;
use Flarum\Extension\Extension;
use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -58,7 +59,6 @@ class Migrator
* @param \Flarum\Database\MigrationRepositoryInterface $repository * @param \Flarum\Database\MigrationRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionResolverInterface $resolver * @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @param \Illuminate\Filesystem\Filesystem $files * @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/ */
public function __construct( public function __construct(
MigrationRepositoryInterface $repository, MigrationRepositoryInterface $repository,
@ -74,16 +74,16 @@ class Migrator
* Run the outstanding migrations at a given path. * Run the outstanding migrations at a given path.
* *
* @param string $path * @param string $path
* @param string $extension * @param Extension $extension
* @return void * @return void
*/ */
public function run($path, $extension = null) public function run($path, Extension $extension = null)
{ {
$this->notes = []; $this->notes = [];
$files = $this->getMigrationFiles($path); $files = $this->getMigrationFiles($path);
$ran = $this->repository->getRan($extension); $ran = $this->repository->getRan($extension ? $extension->getId() : null);
$migrations = array_diff($files, $ran); $migrations = array_diff($files, $ran);
@ -96,10 +96,9 @@ class Migrator
* Run an array of migrations. * Run an array of migrations.
* *
* @param array $migrations * @param array $migrations
* @param bool $pretend * @param Extension $extension
* @return void
*/ */
public function runMigrationList($migrations, $extension) public function runMigrationList($migrations, Extension $extension = null)
{ {
// First we will just make sure that there are any migrations to run. If there // First we will just make sure that there are any migrations to run. If there
// aren't, we will just make a note of it to the developer so they're aware // aren't, we will just make a note of it to the developer so they're aware
@ -122,10 +121,10 @@ class Migrator
* Run "up" a migration instance. * Run "up" a migration instance.
* *
* @param string $file * @param string $file
* @param string $extension * @param Extension $extension
* @return void * @return void
*/ */
protected function runUp($file, $extension) protected function runUp($file, Extension $extension = null)
{ {
// First we will resolve a "real" instance of the migration class from this // First we will resolve a "real" instance of the migration class from this
// migration file name. Once we have the instances we can run the actual // migration file name. Once we have the instances we can run the actual
@ -137,7 +136,7 @@ class Migrator
// Once we have run a migrations class, we will log that it was run in this // Once we have run a migrations class, we will log that it was run in this
// repository so that we don't try to run it next time we do a migration // repository so that we don't try to run it next time we do a migration
// in the application. A migration repository keeps the migrate order. // in the application. A migration repository keeps the migrate order.
$this->repository->log($file, $extension); $this->repository->log($file, $extension ? $extension->getId() : null);
$this->note("<info>Migrated:</info> $file"); $this->note("<info>Migrated:</info> $file");
} }
@ -145,14 +144,15 @@ class Migrator
/** /**
* Rolls all of the currently applied migrations back. * Rolls all of the currently applied migrations back.
* *
* @param bool $pretend * @param string $path
* @param Extension $extension
* @return int * @return int
*/ */
public function reset($path, $extension = null) public function reset($path, Extension $extension = null)
{ {
$this->notes = []; $this->notes = [];
$migrations = array_reverse($this->repository->getRan($extension)); $migrations = array_reverse($this->repository->getRan($extension->getId()));
$this->requireFiles($path, $migrations); $this->requireFiles($path, $migrations);
@ -173,10 +173,10 @@ class Migrator
* Run "down" a migration instance. * Run "down" a migration instance.
* *
* @param string $file * @param string $file
* @param string $extension * @param Extension $extension
* @return void * @return void
*/ */
protected function runDown($file, $extension = null) protected function runDown($file, Extension $extension = null)
{ {
// First we will get the file name of the migration so we can resolve out an // First we will get the file name of the migration so we can resolve out an
// instance of the migration. Once we get an instance we can either run a // instance of the migration. Once we get an instance we can either run a
@ -188,7 +188,7 @@ class Migrator
// Once we have successfully run the migration "down" we will remove it from // Once we have successfully run the migration "down" we will remove it from
// the migration repository so it will be considered to have not been run // the migration repository so it will be considered to have not been run
// by the application then will be able to fire by any later operation. // by the application then will be able to fire by any later operation.
$this->repository->delete($file, $extension); $this->repository->delete($file, $extension ? $extension->getId() : null);
$this->note("<info>Rolled back:</info> $file"); $this->note("<info>Rolled back:</info> $file");
} }
@ -240,13 +240,21 @@ class Migrator
* Resolve a migration instance from a file. * Resolve a migration instance from a file.
* *
* @param string $file * @param string $file
* @param Extension $extension
* @return object * @return object
*/ */
public function resolve($file, $extension = null) public function resolve($file, Extension $extension = null)
{ {
$file = implode('_', array_slice(explode('_', $file), 4)); $file = implode('_', array_slice(explode('_', $file), 4));
$class = ($extension ? str_replace('-', '\\', $extension) : 'Flarum\\Core') . '\\Migration\\'; // flagrow/image-upload
if ($extension) {
$class = str_replace('/', '\\', $extension->name);
} else {
$class = 'Flarum\\Core';
}
$class .= '\\Migration\\';
$class .= Str::studly($file); $class .= Str::studly($file);

View File

@ -10,6 +10,8 @@
namespace Flarum\Event; namespace Flarum\Event;
use Flarum\Extension\Extension;
class ExtensionWasDisabled class ExtensionWasDisabled
{ {
/** /**
@ -18,9 +20,9 @@ class ExtensionWasDisabled
protected $extension; protected $extension;
/** /**
* @param string $extension * @param Extension $extension
*/ */
public function __construct($extension) public function __construct(Extension $extension)
{ {
$this->extension = $extension; $this->extension = $extension;
} }

View File

@ -10,6 +10,8 @@
namespace Flarum\Event; namespace Flarum\Event;
use Flarum\Extension\Extension;
class ExtensionWasEnabled class ExtensionWasEnabled
{ {
/** /**
@ -18,9 +20,9 @@ class ExtensionWasEnabled
protected $extension; protected $extension;
/** /**
* @param string $extension * @param Extension $extension
*/ */
public function __construct($extension) public function __construct(Extension $extension)
{ {
$this->extension = $extension; $this->extension = $extension;
} }

View File

@ -0,0 +1,252 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Extension;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* @property string $name
* @property string $description
* @property string $type
* @property array $keywords
* @property string $homepage
* @property string $time
* @property string $license
* @property array $authors
* @property array $support
* @property array $require
* @property array $requireDev
* @property array $autoload
* @property array $autoloadDev
* @property array $conflict
* @property array $replace
* @property array $provide
* @property array $suggest
* @property array $extra
*/
class Extension implements Arrayable
{
/**
* Unique Id of the extension.
*
* @info Identical to the directory in the extensions directory.
* @example flarum_suspend
*
* @var string
*/
protected $id;
/**
* The directory of this extension.
*
* @var string
*/
protected $path;
/**
* Composer json of the package.
*
* @var array
*/
protected $composerJson;
/**
* Whether the extension is installed.
*
* @var bool
*/
protected $installed = true;
/**
* The installed version of the extension.
*
* @var string
*/
protected $version;
/**
* Whether the extension is enabled.
*
* @var bool
*/
protected $enabled = false;
/**
* @param $path
* @param array $composerJson
*/
public function __construct($path, $composerJson)
{
$this->id = end(explode('/', $path));
$this->path = $path;
$this->composerJson = $composerJson;
}
/**
* {@inheritdoc}
*/
public function __get($name)
{
return $this->composerJsonAttribute(Str::snake($name, '-'));
}
/**
* {@inheritdoc}
*/
public function __isset($name)
{
return isset($this->{$name}) || $this->composerJsonAttribute(Str::snake($name, '-'));
}
/**
* Dot notation getter for composer.json attributes.
*
* @see https://laravel.com/docs/5.1/helpers#arrays
*
* @param $name
* @return mixed
*/
public function composerJsonAttribute($name)
{
return Arr::get($this->composerJson, $name);
}
/**
* @param boolean $installed
* @return Extension
*/
public function setInstalled($installed)
{
$this->installed = $installed;
return $this;
}
/**
* @return boolean
*/
public function isInstalled()
{
return $this->installed;
}
/**
* @param string $version
* @return Extension
*/
public function setVersion($version)
{
$this->version = $version;
return $this;
}
/**
* @return string
*/
public function getVersion()
{
return $this->version;
}
/**
* Loads the icon information from the composer.json.
*
* @return array|null
*/
public function getIcon()
{
if (($icon = $this->composerJsonAttribute('extra.flarum-extension.icon'))) {
if ($file = Arr::get($icon, 'image')) {
$file = $this->path . '/' . $file;
if (file_exists($file)) {
$mimetype = pathinfo($file, PATHINFO_EXTENSION) === 'svg'
? 'image/svg+xml'
: finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file);
$data = file_get_contents($file);
$icon['backgroundImage'] = 'url(\'data:' . $mimetype . ';base64,' . base64_encode($data) . '\')';
}
}
return $icon;
}
}
/**
* @param boolean $enabled
* @return Extension
*/
public function setEnabled($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* @return boolean
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* The raw path of the directory under extensions.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Tests whether the extension has assets.
*
* @return bool
*/
public function hasAssets()
{
return realpath($this->path . '/assets/') !== false;
}
/**
* Tests whether the extension has migrations.
*
* @return bool
*/
public function hasMigrations()
{
return realpath($this->path . '/migrations/') !== false;
}
/**
* Generates an array result for the object.
*
* @return array
*/
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(),
], $this->composerJson);
}
}

View File

@ -19,6 +19,8 @@ use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class ExtensionManager class ExtensionManager
{ {
@ -38,8 +40,13 @@ class ExtensionManager
*/ */
protected $filesystem; protected $filesystem;
public function __construct(SettingsRepositoryInterface $config, Application $app, Migrator $migrator, Dispatcher $dispatcher, Filesystem $filesystem) public function __construct(
{ SettingsRepositoryInterface $config,
Application $app,
Migrator $migrator,
Dispatcher $dispatcher,
Filesystem $filesystem
) {
$this->config = $config; $this->config = $config;
$this->app = $app; $this->app = $app;
$this->migrator = $migrator; $this->migrator = $migrator;
@ -47,61 +54,70 @@ class ExtensionManager
$this->filesystem = $filesystem; $this->filesystem = $filesystem;
} }
public function getInfo() /**
* @return Collection
*/
public function getExtensions()
{ {
$extensionsDir = $this->getExtensionsDir(); $extensionsDir = $this->getExtensionsDir();
$dirs = array_diff(scandir($extensionsDir), ['.', '..']); $dirs = array_diff(scandir($extensionsDir), ['.', '..']);
$extensions = []; $extensions = new Collection();
$installed = json_decode(file_get_contents(public_path('vendor/composer/installed.json')), true); $installed = json_decode(file_get_contents(public_path('vendor/composer/installed.json')), true);
foreach ($dirs as $dir) { foreach ($dirs as $dir) {
if (file_exists($manifest = $extensionsDir . '/' . $dir . '/composer.json')) { if (file_exists($manifest = $extensionsDir . '/' . $dir . '/composer.json')) {
$extension = json_decode(file_get_contents($manifest), true); $extension = new Extension(
$extensionsDir . '/' . $dir,
json_decode(file_get_contents($manifest), true)
);
if (empty($extension['name'])) { if (empty($extension->name)) {
continue; continue;
} }
if (isset($extension['extra']['flarum-extension']['icon'])) {
$icon = &$extension['extra']['flarum-extension']['icon'];
if ($file = array_get($icon, 'image')) {
$file = $extensionsDir . '/' . $dir . '/' . $file;
if (file_exists($file)) {
$mimetype = pathinfo($file, PATHINFO_EXTENSION) === 'svg'
? 'image/svg+xml'
: finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file);
$data = file_get_contents($file);
$icon['backgroundImage'] = 'url(\'data:' . $mimetype . ';base64,' . base64_encode($data) . '\')';
}
}
}
foreach ($installed as $package) { foreach ($installed as $package) {
if ($package['name'] === $extension['name']) { if ($package['name'] === $extension->name) {
$extension['version'] = $package['version']; $extension->setInstalled(true);
$extension->setVersion($package['version']);
$extension->setEnabled($this->isEnabled($dir));
} }
} }
$extension['id'] = $dir; $extensions->put($dir, $extension);
$extensions[$dir] = $extension;
} }
} }
return $extensions; return $extensions->sortBy(function ($extension, $name) {
return $extension->composerJsonAttribute('extra.flarum-extension.title');
});
} }
public function enable($extension) /**
* Loads an Extension with all information.
*
* @param string $name
* @return Extension|null
*/
public function getExtension($name)
{ {
if (! $this->isEnabled($extension)) { return $this->getExtensions()->get($name);
}
/**
* Enables the extension.
*
* @param string $name
*/
public function enable($name)
{
if (!$this->isEnabled($name)) {
$extension = $this->getExtension($name);
$enabled = $this->getEnabled(); $enabled = $this->getEnabled();
$enabled[] = $extension; $enabled[] = $name;
$this->migrate($extension); $this->migrate($extension);
@ -109,53 +125,75 @@ class ExtensionManager
$this->setEnabled($enabled); $this->setEnabled($enabled);
$extension->setEnabled(true);
$this->dispatcher->fire(new ExtensionWasEnabled($extension)); $this->dispatcher->fire(new ExtensionWasEnabled($extension));
} }
} }
public function disable($extension) /**
* Disables an extension.
*
* @param string $name
*/
public function disable($name)
{ {
$enabled = $this->getEnabled(); $enabled = $this->getEnabled();
if (($k = array_search($extension, $enabled)) !== false) { if (($k = array_search($name, $enabled)) !== false) {
unset($enabled[$k]); unset($enabled[$k]);
$extension = $this->getExtension($name);
$this->setEnabled($enabled); $this->setEnabled($enabled);
$extension->setEnabled(false);
$this->dispatcher->fire(new ExtensionWasDisabled($extension)); $this->dispatcher->fire(new ExtensionWasDisabled($extension));
} }
} }
public function uninstall($extension) /**
* Uninstalls an extension.
*
* @param string $name
*/
public function uninstall($name)
{ {
$this->disable($extension); $extension = $this->getExtension($name);
$this->disable($name);
$this->migrateDown($extension); $this->migrateDown($extension);
$this->unpublishAssets($extension); $this->unpublishAssets($extension);
$extension->setInstalled(false);
$this->dispatcher->fire(new ExtensionWasUninstalled($extension)); $this->dispatcher->fire(new ExtensionWasUninstalled($extension));
} }
/** /**
* Copy the assets from an extension's assets directory into public view. * Copy the assets from an extension's assets directory into public view.
* *
* @param string $extension * @param Extension $extension
*/ */
protected function publishAssets($extension) protected function publishAssets(Extension $extension)
{ {
if ($extension->hasAssets()) {
$this->filesystem->copyDirectory( $this->filesystem->copyDirectory(
$this->app->basePath().'/extensions/'.$extension.'/assets', $this->app->basePath() . '/extensions/' . $extension->getId() . '/assets',
$this->app->basePath().'/assets/extensions/'.$extension $this->app->basePath() . '/assets/extensions/' . $extension->getId()
); );
} }
}
/** /**
* Delete an extension's assets from public view. * Delete an extension's assets from public view.
* *
* @param string $extension * @param Extension $extension
*/ */
protected function unpublishAssets($extension) protected function unpublishAssets(Extension $extension)
{ {
$this->filesystem->deleteDirectory($this->app->basePath() . '/assets/extensions/' . $extension); $this->filesystem->deleteDirectory($this->app->basePath() . '/assets/extensions/' . $extension);
} }
@ -163,18 +201,25 @@ class ExtensionManager
/** /**
* Get the path to an extension's published asset. * Get the path to an extension's published asset.
* *
* @param string $extension * @param Extension $extension
* @param string $path * @param string $path
* @return string * @return string
*/ */
public function getAsset($extension, $path) public function getAsset(Extension $extension, $path)
{ {
return $this->app->basePath().'/assets/extensions/'.$extension.$path; return $this->app->basePath() . '/assets/extensions/' . $extension->getId() . $path;
} }
public function migrate($extension, $up = true) /**
* Runs the database migrations for the extension.
*
* @param Extension $extension
* @param bool|true $up
*/
public function migrate(Extension $extension, $up = true)
{ {
$migrationDir = public_path('extensions/' . $extension . '/migrations'); if ($extension->hasMigrations()) {
$migrationDir = public_path('extensions/' . $extension->getId() . '/migrations');
$this->app->bind('Illuminate\Database\Schema\Builder', function ($container) { $this->app->bind('Illuminate\Database\Schema\Builder', function ($container) {
return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder();
@ -186,10 +231,16 @@ class ExtensionManager
$this->migrator->reset($migrationDir, $extension); $this->migrator->reset($migrationDir, $extension);
} }
} }
}
public function migrateDown($extension) /**
* Runs the database migrations to reset the database to its old state.
*
* @param Extension $extension
*/
public function migrateDown(Extension $extension)
{ {
$this->migrate($extension, false); $this->migrate($extension->getId(), false);
} }
public function getMigrator() public function getMigrator()

View File

@ -343,7 +343,7 @@ class InstallCommand extends AbstractCommand
'flarum-pusher', 'flarum-pusher',
]; ];
foreach ($extensions->getInfo() as $name => $extension) { foreach ($extensions->getExtensions() as $name => $extension) {
if (in_array($name, $disabled)) { if (in_array($name, $disabled)) {
continue; continue;
} }

View File

@ -74,8 +74,8 @@ class MigrateCommand extends AbstractCommand
$migrator = $extensions->getMigrator(); $migrator = $extensions->getMigrator();
foreach ($extensions->getInfo() as $name => $extension) { foreach ($extensions->getExtensions() as $name => $extension) {
if (! $extensions->isEnabled($name)) { if (! $extension->isEnabled()) {
continue; continue;
} }