diff --git a/framework/core/js/admin/src/app.js b/framework/core/js/admin/src/app.js index 16c8a9a59..62ace9270 100644 --- a/framework/core/js/admin/src/app.js +++ b/framework/core/js/admin/src/app.js @@ -12,4 +12,6 @@ app.initializers.add('routes', routes); app.initializers.add('preload', preload, -100); app.initializers.add('boot', boot, -100); +app.extensionSettings = {}; + export default app; diff --git a/framework/core/js/admin/src/components/AddExtensionModal.js b/framework/core/js/admin/src/components/AddExtensionModal.js new file mode 100644 index 000000000..2393c5f9d --- /dev/null +++ b/framework/core/js/admin/src/components/AddExtensionModal.js @@ -0,0 +1,21 @@ +import Modal from 'flarum/components/Modal'; + +export default class AddExtensionModal extends Modal { + className() { + return 'AddExtensionModal Modal--small'; + } + + title() { + return 'Add Extension'; + } + + content() { + return ( +
+

One day, this dialog will allow you to add an extension to your forum with ease. We're building an ecosystem as we speak!

+

In the meantime, if you manage to get your hands on a new extension, simply drop it in your forum's extensions directory.

+

If you're a developer, you can read the docs and have a go at building your own.

+
+ ); + } +} diff --git a/framework/core/js/admin/src/components/ExtensionsPage.js b/framework/core/js/admin/src/components/ExtensionsPage.js index eb7101c3c..660ada63b 100644 --- a/framework/core/js/admin/src/components/ExtensionsPage.js +++ b/framework/core/js/admin/src/components/ExtensionsPage.js @@ -1,9 +1,101 @@ import Component from 'flarum/Component'; +import LinkButton from 'flarum/components/LinkButton'; +import Button from 'flarum/components/Button'; +import Dropdown from 'flarum/components/Dropdown'; +import Separator from 'flarum/components/Separator'; +import AddExtensionModal from 'flarum/components/AddExtensionModal'; +import ItemList from 'flarum/utils/ItemList'; export default class ExtensionsPage extends Component { view() { return ( -
+
+
+
+ {Button.component({ + children: 'Add Extension', + icon: 'plus', + className: 'Button Button--primary', + onclick: () => app.modal.show(new AddExtensionModal()) + })} +
+
+ +
+
+
    + {app.extensions + .sort((a, b) => a.name.localeCompare(b.name)) + .map(extension => ( +
  • + {Dropdown.component({ + icon: 'ellipsis-v', + children: this.controlItems(extension).toArray(), + className: 'ExtensionListItem-controls', + buttonClassName: 'Button Button--icon Button--flat', + menuClassName: 'Dropdown-menu--right' + })} +
    + +

    + {extension.title}{' '} + {extension.version} +

    +
    {extension.description}
    +
    +
  • + ))} +
+
+
+
); } + + controlItems(extension) { + const items = new ItemList(); + const enabled = this.isEnabled(extension.name); + + if (app.extensionSettings[extension.name]) { + items.add('settings', Button.component({ + icon: 'cog', + children: 'Settings', + onclick: app.extensionSettings[extension.name] + })); + } + + items.add('toggle', Button.component({ + icon: enabled ? 'times' : 'check', + children: enabled ? 'Disable' : 'Enable', + onclick: () => { + app.request({ + url: app.forum.attribute('apiUrl') + '/extensions/' + extension.name, + method: 'PATCH', + data: {enabled: !enabled} + }).then(() => window.location.reload()); + } + })); + + if (!enabled) { + items.add('uninstall', Button.component({ + icon: 'trash-o', + children: 'Uninstall' + })); + } + + items.add('separator2', Separator.component()); + + items.add('support', LinkButton.component({ + icon: 'support', + children: 'Support' + })); + + return items; + } + + isEnabled(name) { + const enabled = JSON.parse(app.config.extensions_enabled); + + return enabled.indexOf(name) !== -1; + } } diff --git a/framework/core/less/admin/ExtensionsPage.less b/framework/core/less/admin/ExtensionsPage.less new file mode 100644 index 000000000..05a78fec3 --- /dev/null +++ b/framework/core/less/admin/ExtensionsPage.less @@ -0,0 +1,68 @@ +.ExtensionsPage-header { + padding: 20px 0; + background: @control-bg; +} + +.ExtensionsPage-list { + padding: 30px 0; +} + +.ExtensionList { + margin: 0; + padding: 0; + list-style: none; + .clearfix(); + + > li { + float: left; + width: 350px; + height: 80px; + } +} +.ExtensionListItem.disabled .ExtensionListItem-content { + opacity: 0.5; + color: @muted-color; +} +.ExtensionListItem-controls { + float: right; + margin-top: -5px; + visibility: hidden; + z-index: 1; + + .ExtensionListItem:hover &, &.open { + visibility: visible; + } +} +.ExtensionListItem { + padding-left: 42px; + padding-right: 20px; + padding-bottom: 20px; +} +.ExtensionListItem-icon { + float: left; + margin-left: -42px; +} +.ExtensionListItem-title { + font-size: 14px; + margin: 3px 0 5px; + + small { + color: @muted-more-color; + font-size: 11px; + font-weight: normal; + margin-left: 5px; + } +} +.ExtensionListItem-description { + font-size: 12px; + color: @muted-color; +} + +.ExtensionIcon { + width: 28px; + height: 28px; + background: @control-bg; + color: @control-color; + border-radius: 6px; + display: inline-block; +} diff --git a/framework/core/less/admin/app.less b/framework/core/less/admin/app.less index 058b10f4a..e7144b161 100644 --- a/framework/core/less/admin/app.less +++ b/framework/core/less/admin/app.less @@ -5,3 +5,4 @@ @import "BasicsPage.less"; @import "PermissionsPage.less"; @import "EditGroupModal.less"; +@import "ExtensionsPage.less"; diff --git a/framework/core/less/lib/Dropdown.less b/framework/core/less/lib/Dropdown.less index 2e102d2c7..b5a7c9b3e 100755 --- a/framework/core/less/lib/Dropdown.less +++ b/framework/core/less/lib/Dropdown.less @@ -24,7 +24,7 @@ } > li { - > a, > button { + > a, > button, > span { padding: 8px 15px; display: block; width: 100%; @@ -42,12 +42,6 @@ &.hasIcon { padding-left: 40px; } - &:hover { - background: @control-bg; - } - &:focus { - outline: none; - } .Button-icon { float: left; @@ -60,6 +54,14 @@ background: none; } } + > a, > button { + &:hover { + background: @control-bg; + } + &:focus { + outline: none; + } + } &.active { > a, > button { background: @control-bg; diff --git a/framework/core/src/Admin/Actions/ClientAction.php b/framework/core/src/Admin/Actions/ClientAction.php index c384f01d8..6df9c13a4 100644 --- a/framework/core/src/Admin/Actions/ClientAction.php +++ b/framework/core/src/Admin/Actions/ClientAction.php @@ -33,6 +33,7 @@ class ClientAction extends BaseClientAction $view->setVariable('config', $this->settings->all()); $view->setVariable('locales', app('flarum.localeManager')->getLocales()); $view->setVariable('permissions', Permission::map()); + $view->setVariable('extensions', app('flarum.extensions')->getInfo()); return $view; } diff --git a/framework/core/src/Api/Actions/Extensions/UpdateAction.php b/framework/core/src/Api/Actions/Extensions/UpdateAction.php new file mode 100644 index 000000000..8d6260eb5 --- /dev/null +++ b/framework/core/src/Api/Actions/Extensions/UpdateAction.php @@ -0,0 +1,43 @@ +extensions = $extensions; + } + + protected function respond(Request $request) + { + if (! $request->actor->isAdmin()) { + throw new PermissionDeniedException; + } + + $enabled = $request->get('enabled'); + $name = $request->get('name'); + + if ($enabled === true) { + $this->extensions->enable($name); + } elseif ($enabled === false) { + $this->extensions->disable($name); + } + + app('flarum.formatter')->flush(); + + $assetPath = public_path('assets'); + $manifest = file_get_contents($assetPath . '/rev-manifest.json'); + $revisions = json_decode($manifest, true); + + foreach ($revisions as $file => $revision) { + @unlink($assetPath . '/' . substr_replace($file, '-' . $revision, strrpos($file, '.'), 0)); + } + } +} diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index 749ca364f..55325dcad 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -313,6 +313,19 @@ class ApiServiceProvider extends ServiceProvider $this->action('Flarum\Api\Actions\Groups\DeleteAction') ); + /* + |-------------------------------------------------------------------------- + | Extensions + |-------------------------------------------------------------------------- + */ + + // Toggle an extension + $routes->patch( + '/extensions/{name}', + 'flarum.api.extensions.update', + $this->action('Flarum\Api\Actions\Extensions\UpdateAction') + ); + event(new RegisterApiRoutes($routes)); } diff --git a/framework/core/src/Core/Formatter/Formatter.php b/framework/core/src/Core/Formatter/Formatter.php index fcd5ee70d..59ad924ed 100644 --- a/framework/core/src/Core/Formatter/Formatter.php +++ b/framework/core/src/Core/Formatter/Formatter.php @@ -31,6 +31,12 @@ class Formatter return $configurator; } + public function flush() + { + $this->cache->forget('flarum.formatter.parser'); + $this->cache->forget('flarum.formatter.renderer'); + } + protected function getComponent($key) { $cacheKey = 'flarum.formatter.' . $key; diff --git a/framework/core/src/Support/Extension.php b/framework/core/src/Support/Extension.php index 65b947b28..2d065bf3f 100644 --- a/framework/core/src/Support/Extension.php +++ b/framework/core/src/Support/Extension.php @@ -4,4 +4,11 @@ use Flarum\Support\ServiceProvider; class Extension extends ServiceProvider { + public function install() + { + } + + public function uninstall() + { + } } diff --git a/framework/core/src/Support/ExtensionManager.php b/framework/core/src/Support/ExtensionManager.php new file mode 100644 index 000000000..96acb24e2 --- /dev/null +++ b/framework/core/src/Support/ExtensionManager.php @@ -0,0 +1,104 @@ +config = $config; + $this->app = $app; + } + + public function getInfo() + { + $extensionsDir = $this->getExtensionsDir(); + + $dirs = array_diff(scandir($extensionsDir), ['.', '..']); + $extensions = []; + + foreach ($dirs as $dir) { + if (file_exists($manifest = $extensionsDir . '/' . $dir . '/flarum.json')) { + $extensions[] = json_decode(file_get_contents($manifest)); + } + } + + return $extensions; + } + + public function enable($extension) + { + $enabled = $this->getEnabled(); + + if (! in_array($extension, $enabled)) { + $enabled[] = $extension; + + $class = $this->load($extension); + + $class->install(); + + // run migrations + // vendor publish + + $this->setEnabled($enabled); + } + } + + public function disable($extension) + { + $enabled = $this->getEnabled(); + + if (($k = array_search($extension, $enabled)) !== false) { + unset($enabled[$k]); + + $this->setEnabled($enabled); + } + } + + public function uninstall($extension) + { + $this->disable($extension); + + $class = $this->load($extension); + + $class->uninstall(); + + // run migrations + } + + protected function getEnabled() + { + $config = $this->config->get('extensions_enabled'); + + return json_decode($config, true); + } + + protected function setEnabled(array $enabled) + { + $enabled = array_values(array_unique($enabled)); + + $this->config->set('extensions_enabled', json_encode($enabled)); + } + + protected function load($extension) + { + if (file_exists($file = $this->getExtensionsDir() . '/' . $extension . '/bootstrap.php')) { + $className = require $file; + + $class = new $className($this->app); + } + + return $class; + } + + protected function getExtensionsDir() + { + return public_path('extensions'); + } +} diff --git a/framework/core/src/Support/ExtensionsServiceProvider.php b/framework/core/src/Support/ExtensionsServiceProvider.php index 8399c4897..b6ef54290 100644 --- a/framework/core/src/Support/ExtensionsServiceProvider.php +++ b/framework/core/src/Support/ExtensionsServiceProvider.php @@ -9,6 +9,8 @@ class ExtensionsServiceProvider extends ServiceProvider */ public function register() { + $this->app->bind('flarum.extensions', 'Flarum\Support\ExtensionManager'); + $config = $this->app->make('Flarum\Core\Settings\SettingsRepository')->get('extensions_enabled'); $extensions = json_decode($config, true); $providers = [];