diff --git a/framework/core/js/admin/src/components/ExtensionsPage.js b/framework/core/js/admin/src/components/ExtensionsPage.js index 58b1f9832..22ebe0850 100644 --- a/framework/core/js/admin/src/components/ExtensionsPage.js +++ b/framework/core/js/admin/src/components/ExtensionsPage.js @@ -11,7 +11,6 @@ import listItems from 'flarum/helpers/listItems'; export default class ExtensionsPage extends Component { view() { - const extensions = Object.keys(app.extensions).map(id => app.extensions[id]); return (
@@ -29,15 +28,15 @@ export default class ExtensionsPage extends Component {
    - {extensions - .sort((a, b) => a.extra['flarum-extension'].title.localeCompare(b.extra['flarum-extension'].title)) - .map(extension => { + {Object.keys(app.extensions) + .map(id => { + const extension = app.extensions[id]; const controls = this.controlItems(extension.id).toArray(); return
  • - - {extension.extra['flarum-extension'].icon ? icon(extension.extra['flarum-extension'].icon.name) : ''} + + {extension.icon ? icon(extension.icon.name) : ''} {controls.length ? ( setVariable('settings', $settings); $view->setVariable('permissions', Permission::map()); - $view->setVariable('extensions', $this->extensions->getInfo()); + $view->setVariable('extensions', $this->extensions->getExtensions()->toArray()); return $view; } diff --git a/framework/core/src/Database/DatabaseMigrationRepository.php b/framework/core/src/Database/DatabaseMigrationRepository.php index d7230ad01..49a1978e4 100755 --- a/framework/core/src/Database/DatabaseMigrationRepository.php +++ b/framework/core/src/Database/DatabaseMigrationRepository.php @@ -39,9 +39,8 @@ class DatabaseMigrationRepository implements MigrationRepositoryInterface /** * Create a new database migration repository instance. * - * @param \Illuminate\Database\ConnectionResolverInterface $resolver - * @param string $table - * @return void + * @param \Illuminate\Database\ConnectionResolverInterface $resolver + * @param string $table */ public function __construct(Resolver $resolver, $table) { diff --git a/framework/core/src/Database/Migrator.php b/framework/core/src/Database/Migrator.php index 07d6c7b67..979ef2191 100755 --- a/framework/core/src/Database/Migrator.php +++ b/framework/core/src/Database/Migrator.php @@ -11,6 +11,7 @@ namespace Flarum\Database; +use Flarum\Extension\Extension; use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; @@ -55,35 +56,34 @@ class Migrator /** * Create a new migrator instance. * - * @param \Flarum\Database\MigrationRepositoryInterface $repository - * @param \Illuminate\Database\ConnectionResolverInterface $resolver - * @param \Illuminate\Filesystem\Filesystem $files - * @return void + * @param \Flarum\Database\MigrationRepositoryInterface $repository + * @param \Illuminate\Database\ConnectionResolverInterface $resolver + * @param \Illuminate\Filesystem\Filesystem $files */ public function __construct( MigrationRepositoryInterface $repository, Resolver $resolver, Filesystem $files ) { - $this->files = $files; - $this->resolver = $resolver; + $this->files = $files; + $this->resolver = $resolver; $this->repository = $repository; } /** * Run the outstanding migrations at a given path. * - * @param string $path - * @param string $extension + * @param string $path + * @param Extension $extension * @return void */ - public function run($path, $extension = null) + public function run($path, Extension $extension = null) { $this->notes = []; $files = $this->getMigrationFiles($path); - $ran = $this->repository->getRan($extension); + $ran = $this->repository->getRan($extension ? $extension->getId() : null); $migrations = array_diff($files, $ran); @@ -95,11 +95,10 @@ class Migrator /** * Run an array of migrations. * - * @param array $migrations - * @param bool $pretend - * @return void + * @param array $migrations + * @param Extension $extension */ - 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 // aren't, we will just make a note of it to the developer so they're aware @@ -121,11 +120,11 @@ class Migrator /** * Run "up" a migration instance. * - * @param string $file - * @param string $extension + * @param string $file + * @param Extension $extension * @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 // 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 // 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. - $this->repository->log($file, $extension); + $this->repository->log($file, $extension ? $extension->getId() : null); $this->note("Migrated: $file"); } @@ -145,14 +144,15 @@ class Migrator /** * Rolls all of the currently applied migrations back. * - * @param bool $pretend + * @param string $path + * @param Extension $extension * @return int */ - public function reset($path, $extension = null) + public function reset($path, Extension $extension = null) { $this->notes = []; - $migrations = array_reverse($this->repository->getRan($extension)); + $migrations = array_reverse($this->repository->getRan($extension->getId())); $this->requireFiles($path, $migrations); @@ -172,11 +172,11 @@ class Migrator /** * Run "down" a migration instance. * - * @param string $file - * @param string $extension + * @param string $file + * @param Extension $extension * @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 // 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 // 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. - $this->repository->delete($file, $extension); + $this->repository->delete($file, $extension ? $extension->getId() : null); $this->note("Rolled back: $file"); } @@ -196,12 +196,12 @@ class Migrator /** * Get all of the migration files in a given path. * - * @param string $path + * @param string $path * @return array */ public function getMigrationFiles($path) { - $files = $this->files->glob($path.'/*_*.php'); + $files = $this->files->glob($path . '/*_*.php'); // Once we have the array of files in the directory we will just remove the // extension and take the basename of the file which is all we need when @@ -225,28 +225,36 @@ class Migrator /** * Require in all the migration files in a given path. * - * @param string $path - * @param array $files + * @param string $path + * @param array $files * @return void */ public function requireFiles($path, array $files) { foreach ($files as $file) { - $this->files->requireOnce($path.'/'.$file.'.php'); + $this->files->requireOnce($path . '/' . $file . '.php'); } } /** * Resolve a migration instance from a file. * - * @param string $file + * @param string $file + * @param Extension $extension * @return object */ - public function resolve($file, $extension = null) + public function resolve($file, Extension $extension = null) { $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); @@ -256,7 +264,7 @@ class Migrator /** * Raise a note event for the migrator. * - * @param string $message + * @param string $message * @return void */ protected function note($message) @@ -277,7 +285,7 @@ class Migrator /** * Resolve the database connection instance. * - * @param string $connection + * @param string $connection * @return \Illuminate\Database\Connection */ public function resolveConnection($connection) @@ -288,7 +296,7 @@ class Migrator /** * Set the default connection name. * - * @param string $name + * @param string $name * @return void */ public function setConnection($name) diff --git a/framework/core/src/Event/ExtensionWasDisabled.php b/framework/core/src/Event/ExtensionWasDisabled.php index f9f8ba7d0..9382452c4 100644 --- a/framework/core/src/Event/ExtensionWasDisabled.php +++ b/framework/core/src/Event/ExtensionWasDisabled.php @@ -10,6 +10,8 @@ namespace Flarum\Event; +use Flarum\Extension\Extension; + class ExtensionWasDisabled { /** @@ -18,9 +20,9 @@ class ExtensionWasDisabled protected $extension; /** - * @param string $extension + * @param Extension $extension */ - public function __construct($extension) + public function __construct(Extension $extension) { $this->extension = $extension; } diff --git a/framework/core/src/Event/ExtensionWasEnabled.php b/framework/core/src/Event/ExtensionWasEnabled.php index 19d1080be..48f0ea4fa 100644 --- a/framework/core/src/Event/ExtensionWasEnabled.php +++ b/framework/core/src/Event/ExtensionWasEnabled.php @@ -10,6 +10,8 @@ namespace Flarum\Event; +use Flarum\Extension\Extension; + class ExtensionWasEnabled { /** @@ -18,9 +20,9 @@ class ExtensionWasEnabled protected $extension; /** - * @param string $extension + * @param Extension $extension */ - public function __construct($extension) + public function __construct(Extension $extension) { $this->extension = $extension; } diff --git a/framework/core/src/Extension/Extension.php b/framework/core/src/Extension/Extension.php new file mode 100644 index 000000000..33ad9ff9b --- /dev/null +++ b/framework/core/src/Extension/Extension.php @@ -0,0 +1,252 @@ + + * + * 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); + } +} diff --git a/framework/core/src/Extension/ExtensionManager.php b/framework/core/src/Extension/ExtensionManager.php index 288400ac9..cdcc975de 100644 --- a/framework/core/src/Extension/ExtensionManager.php +++ b/framework/core/src/Extension/ExtensionManager.php @@ -19,6 +19,8 @@ use Flarum\Foundation\Application; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; class ExtensionManager { @@ -38,70 +40,84 @@ class ExtensionManager */ protected $filesystem; - public function __construct(SettingsRepositoryInterface $config, Application $app, Migrator $migrator, Dispatcher $dispatcher, Filesystem $filesystem) - { - $this->config = $config; - $this->app = $app; - $this->migrator = $migrator; + public function __construct( + SettingsRepositoryInterface $config, + Application $app, + Migrator $migrator, + Dispatcher $dispatcher, + Filesystem $filesystem + ) { + $this->config = $config; + $this->app = $app; + $this->migrator = $migrator; $this->dispatcher = $dispatcher; $this->filesystem = $filesystem; } - public function getInfo() + /** + * @return Collection + */ + public function getExtensions() { $extensionsDir = $this->getExtensionsDir(); - $dirs = array_diff(scandir($extensionsDir), ['.', '..']); - $extensions = []; + $dirs = array_diff(scandir($extensionsDir), ['.', '..']); + $extensions = new Collection(); $installed = json_decode(file_get_contents(public_path('vendor/composer/installed.json')), true); foreach ($dirs as $dir) { 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; } - 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) { - if ($package['name'] === $extension['name']) { - $extension['version'] = $package['version']; + if ($package['name'] === $extension->name) { + $extension->setInstalled(true); + $extension->setVersion($package['version']); + $extension->setEnabled($this->isEnabled($dir)); } } - $extension['id'] = $dir; - - $extensions[$dir] = $extension; + $extensions->put($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[] = $extension; + $enabled[] = $name; $this->migrate($extension); @@ -109,87 +125,122 @@ class ExtensionManager $this->setEnabled($enabled); + $extension->setEnabled(true); + $this->dispatcher->fire(new ExtensionWasEnabled($extension)); } } - public function disable($extension) + /** + * Disables an extension. + * + * @param string $name + */ + public function disable($name) { $enabled = $this->getEnabled(); - if (($k = array_search($extension, $enabled)) !== false) { + if (($k = array_search($name, $enabled)) !== false) { unset($enabled[$k]); + $extension = $this->getExtension($name); + $this->setEnabled($enabled); + $extension->setEnabled(false); + $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->unpublishAssets($extension); + $extension->setInstalled(false); + $this->dispatcher->fire(new ExtensionWasUninstalled($extension)); } /** * 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) { - $this->filesystem->copyDirectory( - $this->app->basePath().'/extensions/'.$extension.'/assets', - $this->app->basePath().'/assets/extensions/'.$extension - ); + if ($extension->hasAssets()) { + $this->filesystem->copyDirectory( + $this->app->basePath() . '/extensions/' . $extension->getId() . '/assets', + $this->app->basePath() . '/assets/extensions/' . $extension->getId() + ); + } } /** * 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); } /** * Get the path to an extension's published asset. * - * @param string $extension - * @param string $path + * @param Extension $extension + * @param string $path * @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) { - return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); - }); + $this->app->bind('Illuminate\Database\Schema\Builder', function ($container) { + return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); + }); - if ($up) { - $this->migrator->run($migrationDir, $extension); - } else { - $this->migrator->reset($migrationDir, $extension); + if ($up) { + $this->migrator->run($migrationDir, $extension); + } else { + $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() diff --git a/framework/core/src/Install/Console/InstallCommand.php b/framework/core/src/Install/Console/InstallCommand.php index 68802d22c..e67d192c8 100644 --- a/framework/core/src/Install/Console/InstallCommand.php +++ b/framework/core/src/Install/Console/InstallCommand.php @@ -343,7 +343,7 @@ class InstallCommand extends AbstractCommand 'flarum-pusher', ]; - foreach ($extensions->getInfo() as $name => $extension) { + foreach ($extensions->getExtensions() as $name => $extension) { if (in_array($name, $disabled)) { continue; } diff --git a/framework/core/src/Update/Console/MigrateCommand.php b/framework/core/src/Update/Console/MigrateCommand.php index 9638fe157..6356d2c6b 100644 --- a/framework/core/src/Update/Console/MigrateCommand.php +++ b/framework/core/src/Update/Console/MigrateCommand.php @@ -74,8 +74,8 @@ class MigrateCommand extends AbstractCommand $migrator = $extensions->getMigrator(); - foreach ($extensions->getInfo() as $name => $extension) { - if (! $extensions->isEnabled($name)) { + foreach ($extensions->getExtensions() as $name => $extension) { + if (! $extension->isEnabled()) { continue; }