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 = [];