From 651a6bf4eabe186a56cc47054abdd7690bcf00a9 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sat, 30 Jun 2018 12:31:12 +0930 Subject: [PATCH] Frontend refactor (#1471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor Frontend + Asset code - Use Laravel's Filesystem component for asset IO, meaning theoretically assets should be storable on S3 etc. - More reliable checking for asset recompilation when debug mode is on, so you don't have to constantly delete the compiled assets to force a recompile. Should also fix issues with locale JS files being recompiled with the same name and cached. - Remove JavaScript minification, because it will be done by Webpack (exception is for the TextFormatter JS). - Add support for JS sourcemaps. - Separate frontend view and assets completely. This is an important distinction because frontend assets are compiled independent of a request, whereas putting together a view depends on a request. - Bind frontend view/asset factory instances to the container (in service providers) rather than subclassing. Asset and content populators can be added to these factories – these are simply objects that populate the asset compilers or the view with information. - Add RouteHandlerFactory functions that make it easy to hook up a frontend controller with a frontend instance ± some content. - Remove the need for "nojs" - Fix cache:clear command - Recompile assets when settings/enabled extensions change --- framework/core/composer.json | 5 +- framework/core/js/src/common/Translator.js | 5 +- .../core/src/Admin/AdminServiceProvider.php | 58 +-- .../core/src/Admin/CheckCustomLessFormat.php | 43 -- .../core/src/Admin/Content/AdminPayload.php | 60 +++ .../Admin/Controller/FrontendController.php | 79 --- framework/core/src/Admin/routes.php | 3 +- .../Api/Controller/SetSettingsController.php | 11 +- framework/core/src/Extend/Assets.php | 58 +-- framework/core/src/Formatter/Formatter.php | 1 + framework/core/src/Forum/Asset/CustomCss.php | 51 ++ .../core/src/Forum/Asset/FormatterJs.php | 51 ++ .../src/Forum/Content/AssertRegistered.php | 27 + .../Discussion.php} | 54 +- framework/core/src/Forum/Content/Index.php | 97 ++++ .../Controller/AuthorizedWebAppController.php | 30 -- .../Forum/Controller/FrontendController.php | 28 - .../src/Forum/Controller/IndexController.php | 87 ---- .../core/src/Forum/ForumServiceProvider.php | 66 ++- framework/core/src/Forum/Frontend.php | 68 --- .../src/Forum/RecompileFrontendAssets.php | 110 ++++ framework/core/src/Forum/routes.php | 11 +- .../Foundation/Console/CacheClearCommand.php | 24 +- .../Event/ClearingCache.php} | 9 +- framework/core/src/Foundation/Site.php | 7 + .../core/src/Frontend/AbstractFrontend.php | 184 ------- .../Frontend/AbstractFrontendController.php | 57 -- .../src/Frontend/Asset/AssetInterface.php | 39 ++ .../core/src/Frontend/Asset/CoreAssets.php | 48 ++ .../src/Frontend/Asset/ExtensionAssets.php | 80 +++ .../core/src/Frontend/Asset/JsCompiler.php | 51 -- .../core/src/Frontend/Asset/LessCompiler.php | 70 --- .../core/src/Frontend/Asset/LessVariables.php | 65 +++ .../core/src/Frontend/Asset/LocaleAssets.php | 53 ++ .../src/Frontend/Asset/LocaleJsCompiler.php | 33 -- .../src/Frontend/Asset/RevisionCompiler.php | 207 -------- .../core/src/Frontend/Asset/Translations.php | 87 ++++ .../{Asset => Compiler}/CompilerInterface.php | 23 +- .../core/src/Frontend/Compiler/JsCompiler.php | 84 +++ .../src/Frontend/Compiler/LessCompiler.php | 96 ++++ .../Frontend/Compiler/RevisionCompiler.php | 276 ++++++++++ .../Frontend/Compiler/Source/FileSource.php | 52 ++ .../Compiler/Source/SourceCollector.php | 50 ++ .../Compiler/Source/SourceInterface.php | 25 + .../Frontend/Compiler/Source/StringSource.php | 50 ++ .../core/src/Frontend/CompilerFactory.php | 239 +++++++++ .../src/Frontend/Content/ContentInterface.php | 24 + .../core/src/Frontend/Content/CorePayload.php | 99 ++++ .../core/src/Frontend/Content/Layout.php | 36 ++ framework/core/src/Frontend/Content/Meta.php | 50 ++ framework/core/src/Frontend/Controller.php | 43 ++ .../core/src/Frontend/Event/Rendering.php | 81 --- .../core/src/Frontend/FrontendAssets.php | 165 ------ .../src/Frontend/FrontendAssetsFactory.php | 55 -- .../src/Frontend/FrontendServiceProvider.php | 59 +++ framework/core/src/Frontend/FrontendView.php | 488 ------------------ .../core/src/Frontend/FrontendViewFactory.php | 72 --- framework/core/src/Frontend/HtmlDocument.php | 251 +++++++++ .../core/src/Frontend/HtmlDocumentFactory.php | 180 +++++++ .../src/Frontend/RecompileFrontendAssets.php | 84 +++ .../core/src/Http/ControllerRouteHandler.php | 12 +- .../core/src/Http/Middleware/SetLocale.php | 2 + .../core/src/Http/RouteHandlerFactory.php | 39 +- .../core/src/Locale/LocaleServiceProvider.php | 4 +- framework/core/src/Settings/Event/Saved.php | 21 +- .../Event/Saving.php} | 17 +- .../Settings/OverrideSettingsRepository.php | 61 +++ framework/core/views/frontend/admin.blade.php | 54 +- framework/core/views/frontend/app.blade.php | 81 ++- .../core/views/frontend/content.blade.php | 32 +- .../frontend/content/discussion.blade.php | 6 +- .../views/frontend/content/index.blade.php | 6 +- framework/core/views/frontend/forum.blade.php | 88 ++-- 73 files changed, 2846 insertions(+), 2176 deletions(-) delete mode 100644 framework/core/src/Admin/CheckCustomLessFormat.php create mode 100644 framework/core/src/Admin/Content/AdminPayload.php delete mode 100644 framework/core/src/Admin/Controller/FrontendController.php create mode 100644 framework/core/src/Forum/Asset/CustomCss.php create mode 100644 framework/core/src/Forum/Asset/FormatterJs.php create mode 100644 framework/core/src/Forum/Content/AssertRegistered.php rename framework/core/src/Forum/{Controller/DiscussionController.php => Content/Discussion.php} (62%) create mode 100644 framework/core/src/Forum/Content/Index.php delete mode 100644 framework/core/src/Forum/Controller/AuthorizedWebAppController.php delete mode 100644 framework/core/src/Forum/Controller/FrontendController.php delete mode 100644 framework/core/src/Forum/Controller/IndexController.php delete mode 100644 framework/core/src/Forum/Frontend.php create mode 100644 framework/core/src/Forum/RecompileFrontendAssets.php rename framework/core/src/{Event/ConfigureClientView.php => Foundation/Event/ClearingCache.php} (63%) delete mode 100644 framework/core/src/Frontend/AbstractFrontend.php delete mode 100644 framework/core/src/Frontend/AbstractFrontendController.php create mode 100644 framework/core/src/Frontend/Asset/AssetInterface.php create mode 100644 framework/core/src/Frontend/Asset/CoreAssets.php create mode 100644 framework/core/src/Frontend/Asset/ExtensionAssets.php delete mode 100644 framework/core/src/Frontend/Asset/JsCompiler.php delete mode 100644 framework/core/src/Frontend/Asset/LessCompiler.php create mode 100644 framework/core/src/Frontend/Asset/LessVariables.php create mode 100644 framework/core/src/Frontend/Asset/LocaleAssets.php delete mode 100644 framework/core/src/Frontend/Asset/LocaleJsCompiler.php delete mode 100644 framework/core/src/Frontend/Asset/RevisionCompiler.php create mode 100644 framework/core/src/Frontend/Asset/Translations.php rename framework/core/src/Frontend/{Asset => Compiler}/CompilerInterface.php (60%) create mode 100644 framework/core/src/Frontend/Compiler/JsCompiler.php create mode 100644 framework/core/src/Frontend/Compiler/LessCompiler.php create mode 100644 framework/core/src/Frontend/Compiler/RevisionCompiler.php create mode 100644 framework/core/src/Frontend/Compiler/Source/FileSource.php create mode 100644 framework/core/src/Frontend/Compiler/Source/SourceCollector.php create mode 100644 framework/core/src/Frontend/Compiler/Source/SourceInterface.php create mode 100644 framework/core/src/Frontend/Compiler/Source/StringSource.php create mode 100644 framework/core/src/Frontend/CompilerFactory.php create mode 100644 framework/core/src/Frontend/Content/ContentInterface.php create mode 100644 framework/core/src/Frontend/Content/CorePayload.php create mode 100644 framework/core/src/Frontend/Content/Layout.php create mode 100644 framework/core/src/Frontend/Content/Meta.php create mode 100644 framework/core/src/Frontend/Controller.php delete mode 100644 framework/core/src/Frontend/Event/Rendering.php delete mode 100644 framework/core/src/Frontend/FrontendAssets.php delete mode 100644 framework/core/src/Frontend/FrontendAssetsFactory.php delete mode 100644 framework/core/src/Frontend/FrontendView.php delete mode 100644 framework/core/src/Frontend/FrontendViewFactory.php create mode 100644 framework/core/src/Frontend/HtmlDocument.php create mode 100644 framework/core/src/Frontend/HtmlDocumentFactory.php create mode 100644 framework/core/src/Frontend/RecompileFrontendAssets.php rename framework/core/src/{Admin/Frontend.php => Settings/Event/Saving.php} (53%) create mode 100644 framework/core/src/Settings/OverrideSettingsRepository.php diff --git a/framework/core/composer.json b/framework/core/composer.json index 8671d10bd..b9423b70e 100644 --- a/framework/core/composer.json +++ b/framework/core/composer.json @@ -45,7 +45,7 @@ "matthiasmullie/minify": "^1.3", "monolog/monolog": "^1.16.0", "nikic/fast-route": "^0.6", - "oyejorge/less.php": "~1.5", + "oyejorge/less.php": "^1.7", "psr/http-message": "^1.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", @@ -57,7 +57,8 @@ "symfony/yaml": "^3.3", "tobscure/json-api": "^0.3.0", "zendframework/zend-diactoros": "^1.7", - "zendframework/zend-stratigility": "^3.0" + "zendframework/zend-stratigility": "^3.0", + "axy/sourcemap": "^0.1.4" }, "require-dev": { "mockery/mockery": "^0.9.4", diff --git a/framework/core/js/src/common/Translator.js b/framework/core/js/src/common/Translator.js index b9223fc13..07d62e851 100644 --- a/framework/core/js/src/common/Translator.js +++ b/framework/core/js/src/common/Translator.js @@ -1,6 +1,5 @@ import User from './models/User'; import username from './helpers/username'; -import extractText from './utils/extractText'; import extract from './utils/extract'; /** @@ -23,6 +22,10 @@ export default class Translator { this.locale = null; } + addTranslations(translations) { + Object.assign(this.translations, translations); + } + trans(id, parameters) { const translation = this.translations[id]; diff --git a/framework/core/src/Admin/AdminServiceProvider.php b/framework/core/src/Admin/AdminServiceProvider.php index ae0014c10..3849d183c 100644 --- a/framework/core/src/Admin/AdminServiceProvider.php +++ b/framework/core/src/Admin/AdminServiceProvider.php @@ -13,9 +13,8 @@ namespace Flarum\Admin; use Flarum\Admin\Middleware\RequireAdministrateAbility; use Flarum\Event\ConfigureMiddleware; -use Flarum\Extension\Event\Disabled; -use Flarum\Extension\Event\Enabled; use Flarum\Foundation\AbstractServiceProvider; +use Flarum\Frontend\RecompileFrontendAssets; use Flarum\Http\Middleware\AuthenticateWithSession; use Flarum\Http\Middleware\DispatchRoute; use Flarum\Http\Middleware\HandleErrors; @@ -26,7 +25,6 @@ use Flarum\Http\Middleware\StartSession; use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; use Flarum\Http\UrlGenerator; -use Flarum\Settings\Event\Saved; use Zend\Stratigility\MiddlewarePipe; class AdminServiceProvider extends AbstractServiceProvider @@ -64,6 +62,19 @@ class AdminServiceProvider extends AbstractServiceProvider return $pipe; }); + + $this->app->bind('flarum.admin.assets', function () { + return $this->app->make('flarum.frontend.assets.defaults')('admin'); + }); + + $this->app->bind('flarum.admin.frontend', function () { + $view = $this->app->make('flarum.frontend.view.defaults')('admin'); + + $view->setAssets($this->app->make('flarum.admin.assets')); + $view->add($this->app->make(Content\AdminPayload::class)); + + return $view; + }); } /** @@ -75,12 +86,15 @@ class AdminServiceProvider extends AbstractServiceProvider $this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin'); - $this->registerListeners(); + $this->app->make('events')->subscribe( + new RecompileFrontendAssets( + $this->app->make('flarum.admin.assets'), + $this->app->make('flarum.locales') + ) + ); } /** - * Populate the forum client routes. - * * @param RouteCollection $routes */ protected function populateRoutes(RouteCollection $routes) @@ -90,36 +104,4 @@ class AdminServiceProvider extends AbstractServiceProvider $callback = include __DIR__.'/routes.php'; $callback($routes, $factory); } - - protected function registerListeners() - { - $dispatcher = $this->app->make('events'); - - // Flush web app assets when the theme is changed - $dispatcher->listen(Saved::class, function (Saved $event) { - if (preg_match('/^theme_|^custom_less$/i', $event->key)) { - $this->getWebAppAssets()->flushCss(); - } - }); - - // Flush web app assets when extensions are changed - $dispatcher->listen(Enabled::class, [$this, 'flushWebAppAssets']); - $dispatcher->listen(Disabled::class, [$this, 'flushWebAppAssets']); - - // Check the format of custom LESS code - $dispatcher->subscribe(CheckCustomLessFormat::class); - } - - public function flushWebAppAssets() - { - $this->getWebAppAssets()->flush(); - } - - /** - * @return \Flarum\Frontend\FrontendAssets - */ - protected function getWebAppAssets() - { - return $this->app->make(Frontend::class)->getAssets(); - } } diff --git a/framework/core/src/Admin/CheckCustomLessFormat.php b/framework/core/src/Admin/CheckCustomLessFormat.php deleted file mode 100644 index a2644945f..000000000 --- a/framework/core/src/Admin/CheckCustomLessFormat.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Admin; - -use Flarum\Foundation\ValidationException; -use Flarum\Settings\Event\Serializing; -use Illuminate\Contracts\Events\Dispatcher; -use Less_Exception_Parser; -use Less_Parser; - -class CheckCustomLessFormat -{ - public function subscribe(Dispatcher $events) - { - $events->listen(Serializing::class, [$this, 'check']); - } - - public function check(Serializing $event) - { - if ($event->key === 'custom_less') { - $parser = new Less_Parser(); - - try { - // Check the custom less format before saving - // Variables names are not checked, we would have to set them and call getCss() to check them - $parser->parse($event->value); - } catch (Less_Exception_Parser $e) { - throw new ValidationException([ - 'custom_less' => $e->getMessage(), - ]); - } - } - } -} diff --git a/framework/core/src/Admin/Content/AdminPayload.php b/framework/core/src/Admin/Content/AdminPayload.php new file mode 100644 index 000000000..af70617aa --- /dev/null +++ b/framework/core/src/Admin/Content/AdminPayload.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Admin\Content; + +use Flarum\Extension\ExtensionManager; +use Flarum\Frontend\Content\ContentInterface; +use Flarum\Frontend\HtmlDocument; +use Flarum\Group\Permission; +use Flarum\Settings\SettingsRepositoryInterface; +use Illuminate\Database\ConnectionInterface; +use Psr\Http\Message\ServerRequestInterface as Request; + +class AdminPayload implements ContentInterface +{ + /** + * @var SettingsRepositoryInterface + */ + protected $settings; + + /** + * @var ExtensionManager + */ + protected $extensions; + + /** + * @var ConnectionInterface + */ + protected $db; + + /** + * @param SettingsRepositoryInterface $settings + * @param ExtensionManager $extensions + * @param ConnectionInterface $db + */ + public function __construct(SettingsRepositoryInterface $settings, ExtensionManager $extensions, ConnectionInterface $db) + { + $this->settings = $settings; + $this->extensions = $extensions; + $this->db = $db; + } + + public function populate(HtmlDocument $document, Request $request) + { + $document->payload['settings'] = $this->settings->all(); + $document->payload['permissions'] = Permission::map(); + $document->payload['extensions'] = $this->extensions->getExtensions()->toArray(); + + $document->payload['phpVersion'] = PHP_VERSION; + $document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version; + } +} diff --git a/framework/core/src/Admin/Controller/FrontendController.php b/framework/core/src/Admin/Controller/FrontendController.php deleted file mode 100644 index 1ae262cbf..000000000 --- a/framework/core/src/Admin/Controller/FrontendController.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Admin\Controller; - -use Flarum\Admin\Frontend; -use Flarum\Extension\ExtensionManager; -use Flarum\Frontend\AbstractFrontendController; -use Flarum\Group\Permission; -use Flarum\Settings\Event\Deserializing; -use Flarum\Settings\SettingsRepositoryInterface; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Database\ConnectionInterface; -use Psr\Http\Message\ServerRequestInterface; - -class FrontendController extends AbstractFrontendController -{ - /** - * @var SettingsRepositoryInterface - */ - protected $settings; - - /** - * @var ExtensionManager - */ - protected $extensions; - - /** - * @var ConnectionInterface - */ - protected $db; - - /** - * @param Frontend $webApp - * @param Dispatcher $events - * @param SettingsRepositoryInterface $settings - * @param ExtensionManager $extensions - * @param ConnectionInterface $db - */ - public function __construct(Frontend $webApp, Dispatcher $events, SettingsRepositoryInterface $settings, ExtensionManager $extensions, ConnectionInterface $db) - { - $this->webApp = $webApp; - $this->events = $events; - $this->settings = $settings; - $this->extensions = $extensions; - $this->db = $db; - } - - /** - * {@inheritdoc} - */ - protected function getView(ServerRequestInterface $request) - { - $view = parent::getView($request); - - $settings = $this->settings->all(); - - $this->events->dispatch( - new Deserializing($settings) - ); - - $view->setVariable('settings', $settings); - $view->setVariable('permissions', Permission::map()); - $view->setVariable('extensions', $this->extensions->getExtensions()->toArray()); - - $view->setVariable('phpVersion', PHP_VERSION); - $view->setVariable('mysqlVersion', $this->db->selectOne('select version() as version')->version); - - return $view; - } -} diff --git a/framework/core/src/Admin/routes.php b/framework/core/src/Admin/routes.php index eac48ca4e..d994a4916 100644 --- a/framework/core/src/Admin/routes.php +++ b/framework/core/src/Admin/routes.php @@ -9,7 +9,6 @@ * file that was distributed with this source code. */ -use Flarum\Admin\Controller; use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; @@ -17,6 +16,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { $map->get( '/', 'index', - $route->toController(Controller\FrontendController::class) + $route->toAdmin() ); }; diff --git a/framework/core/src/Api/Controller/SetSettingsController.php b/framework/core/src/Api/Controller/SetSettingsController.php index f63d83cbd..76accc8fa 100644 --- a/framework/core/src/Api/Controller/SetSettingsController.php +++ b/framework/core/src/Api/Controller/SetSettingsController.php @@ -11,8 +11,7 @@ namespace Flarum\Api\Controller; -use Flarum\Settings\Event\Saved; -use Flarum\Settings\Event\Serializing; +use Flarum\Settings\Event; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\AssertPermissionTrait; use Illuminate\Contracts\Events\Dispatcher; @@ -53,14 +52,16 @@ class SetSettingsController implements RequestHandlerInterface $settings = $request->getParsedBody(); + $this->dispatcher->dispatch(new Event\Saving($settings)); + foreach ($settings as $k => $v) { - $this->dispatcher->dispatch(new Serializing($k, $v)); + $this->dispatcher->dispatch(new Event\Serializing($k, $v)); $this->settings->set($k, $v); - - $this->dispatcher->dispatch(new Saved($k, $v)); } + $this->dispatcher->dispatch(new Event\Saved($settings)); + return new EmptyResponse(204); } } diff --git a/framework/core/src/Extend/Assets.php b/framework/core/src/Extend/Assets.php index bd5fb6592..1e37665fe 100644 --- a/framework/core/src/Extend/Assets.php +++ b/framework/core/src/Extend/Assets.php @@ -12,29 +12,37 @@ namespace Flarum\Extend; use Flarum\Extension\Extension; -use Flarum\Frontend\Event\Rendering; +use Flarum\Frontend\Asset\ExtensionAssets; +use Flarum\Frontend\CompilerFactory; use Illuminate\Contracts\Container\Container; -use Illuminate\Events\Dispatcher; class Assets implements ExtenderInterface { - protected $appName; + protected $frontend; - protected $assets = []; + protected $css = []; protected $js; - public function __construct($appName) + public function __construct($frontend) { - $this->appName = $appName; + $this->frontend = $frontend; } - public function asset($path) + public function css($path) { - $this->assets[] = $path; + $this->css[] = $path; return $this; } + /** + * @deprecated + */ + public function asset($path) + { + return $this->css($path); + } + public function js($path) { $this->js = $path; @@ -44,35 +52,13 @@ class Assets implements ExtenderInterface public function __invoke(Container $container, Extension $extension = null) { - $container->make(Dispatcher::class)->listen( - Rendering::class, - function (Rendering $event) use ($extension) { - if (! $this->matches($event)) { - return; - } - - $event->addAssets($this->assets); - - if ($this->js) { - $event->view->getJs()->addString(function () use ($extension) { - $name = $extension->getId(); - - return 'var module={};'.file_get_contents($this->js).";\nflarum.extensions['$name']=module.exports"; - }); - } + $container->resolving( + "flarum.$this->frontend.assets", + function (CompilerFactory $assets) use ($extension) { + $assets->add(function () use ($extension) { + return new ExtensionAssets($extension, $this->css, $this->js); + }); } ); } - - private function matches(Rendering $event) - { - switch ($this->appName) { - case 'admin': - return $event->isAdmin(); - case 'forum': - return $event->isForum(); - default: - return false; - } - } } diff --git a/framework/core/src/Formatter/Formatter.php b/framework/core/src/Formatter/Formatter.php index 7de98bdbb..18646742e 100644 --- a/framework/core/src/Formatter/Formatter.php +++ b/framework/core/src/Formatter/Formatter.php @@ -196,6 +196,7 @@ class Formatter $configurator = $this->getConfigurator(); $configurator->enableJavaScript(); $configurator->javascript->exportMethods = ['preview']; + $configurator->javascript->setMinifier('MatthiasMullieMinify'); return $configurator->finalize([ 'returnParser' => false, diff --git a/framework/core/src/Forum/Asset/CustomCss.php b/framework/core/src/Forum/Asset/CustomCss.php new file mode 100644 index 000000000..3a0d84433 --- /dev/null +++ b/framework/core/src/Forum/Asset/CustomCss.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Forum\Asset; + +use Flarum\Frontend\Asset\AssetInterface; +use Flarum\Frontend\Compiler\Source\SourceCollector; +use Flarum\Settings\SettingsRepositoryInterface; + +class CustomCss implements AssetInterface +{ + /** + * @var SettingsRepositoryInterface + */ + protected $settings; + + /** + * @param SettingsRepositoryInterface $settings + */ + public function __construct(SettingsRepositoryInterface $settings) + { + $this->settings = $settings; + } + + public function css(SourceCollector $sources) + { + $sources->addString(function () { + return $this->settings->get('custom_less'); + }); + } + + public function js(SourceCollector $sources) + { + } + + public function localeJs(SourceCollector $sources, string $locale) + { + } + + public function localeCss(SourceCollector $sources, string $locale) + { + } +} diff --git a/framework/core/src/Forum/Asset/FormatterJs.php b/framework/core/src/Forum/Asset/FormatterJs.php new file mode 100644 index 000000000..99e521beb --- /dev/null +++ b/framework/core/src/Forum/Asset/FormatterJs.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Forum\Asset; + +use Flarum\Formatter\Formatter; +use Flarum\Frontend\Asset\AssetInterface; +use Flarum\Frontend\Compiler\Source\SourceCollector; + +class FormatterJs implements AssetInterface +{ + /** + * @var Formatter + */ + protected $formatter; + + /** + * @param Formatter $formatter + */ + public function __construct(Formatter $formatter) + { + $this->formatter = $formatter; + } + + public function js(SourceCollector $sources) + { + $sources->addString(function () { + return $this->formatter->getJs(); + }); + } + + public function css(SourceCollector $sources) + { + } + + public function localeJs(SourceCollector $sources, string $locale) + { + } + + public function localeCss(SourceCollector $sources, string $locale) + { + } +} diff --git a/framework/core/src/Forum/Content/AssertRegistered.php b/framework/core/src/Forum/Content/AssertRegistered.php new file mode 100644 index 000000000..8bceb4761 --- /dev/null +++ b/framework/core/src/Forum/Content/AssertRegistered.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Forum\Content; + +use Flarum\Frontend\Content\ContentInterface; +use Flarum\Frontend\HtmlDocument; +use Flarum\User\AssertPermissionTrait; +use Psr\Http\Message\ServerRequestInterface as Request; + +class AssertRegistered implements ContentInterface +{ + use AssertPermissionTrait; + + public function populate(HtmlDocument $document, Request $request) + { + $this->assertRegistered($request->getAttribute('actor')); + } +} diff --git a/framework/core/src/Forum/Controller/DiscussionController.php b/framework/core/src/Forum/Content/Discussion.php similarity index 62% rename from framework/core/src/Forum/Controller/DiscussionController.php rename to framework/core/src/Forum/Content/Discussion.php index d39ec77c7..9520a7b31 100644 --- a/framework/core/src/Forum/Controller/DiscussionController.php +++ b/framework/core/src/Forum/Content/Discussion.php @@ -9,17 +9,18 @@ * file that was distributed with this source code. */ -namespace Flarum\Forum\Controller; +namespace Flarum\Forum\Content; use Flarum\Api\Client; -use Flarum\Forum\Frontend; +use Flarum\Frontend\Content\ContentInterface; +use Flarum\Frontend\HtmlDocument; use Flarum\Http\Exception\RouteNotFoundException; use Flarum\Http\UrlGenerator; use Flarum\User\User; -use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\View\Factory; use Psr\Http\Message\ServerRequestInterface as Request; -class DiscussionController extends FrontendController +class Discussion implements ContentInterface { /** * @var Client @@ -32,23 +33,24 @@ class DiscussionController extends FrontendController protected $url; /** - * {@inheritdoc} + * @var Factory */ - public function __construct(Frontend $webApp, Dispatcher $events, Client $api, UrlGenerator $url) - { - parent::__construct($webApp, $events); - - $this->api = $api; - $this->url = $url; - } + protected $view; /** - * {@inheritdoc} + * @param Client $api + * @param UrlGenerator $url + * @param Factory $view */ - protected function getView(Request $request) + public function __construct(Client $api, UrlGenerator $url, Factory $view) { - $view = parent::getView($request); + $this->api = $api; + $this->url = $url; + $this->view = $view; + } + public function populate(HtmlDocument $document, Request $request) + { $queryParams = $request->getQueryParams(); $page = max(1, array_get($queryParams, 'page')); @@ -61,35 +63,35 @@ class DiscussionController extends FrontendController ] ]; - $document = $this->getDocument($request->getAttribute('actor'), $params); + $apiDocument = $this->getApiDocument($request->getAttribute('actor'), $params); - $getResource = function ($link) use ($document) { - return array_first($document->included, function ($value, $key) use ($link) { + $getResource = function ($link) use ($apiDocument) { + return array_first($apiDocument->included, function ($value) use ($link) { return $value->type === $link->type && $value->id === $link->id; }); }; - $url = function ($newQueryParams) use ($queryParams, $document) { + $url = function ($newQueryParams) use ($queryParams, $apiDocument) { $newQueryParams = array_merge($queryParams, $newQueryParams); $queryString = http_build_query($newQueryParams); - return $this->url->to('forum')->route('discussion', ['id' => $document->data->id]). + return $this->url->to('forum')->route('discussion', ['id' => $apiDocument->data->id]). ($queryString ? '?'.$queryString : ''); }; $posts = []; - foreach ($document->included as $resource) { + foreach ($apiDocument->included as $resource) { if ($resource->type === 'posts' && isset($resource->relationships->discussion) && isset($resource->attributes->contentHtml)) { $posts[] = $resource; } } - $view->title = $document->data->attributes->title; - $view->document = $document; - $view->content = app('view')->make('flarum.forum::frontend.content.discussion', compact('document', 'page', 'getResource', 'posts', 'url')); + $document->title = $apiDocument->data->attributes->title; + $document->content = $this->view->make('flarum.forum::frontend.content.discussion', compact('apiDocument', 'page', 'getResource', 'posts', 'url')); + $document->payload['apiDocument'] = $apiDocument; - return $view; + return $document; } /** @@ -100,7 +102,7 @@ class DiscussionController extends FrontendController * @return object * @throws RouteNotFoundException */ - protected function getDocument(User $actor, array $params) + protected function getApiDocument(User $actor, array $params) { $response = $this->api->send('Flarum\Api\Controller\ShowDiscussionController', $actor, $params); $statusCode = $response->getStatusCode(); diff --git a/framework/core/src/Forum/Content/Index.php b/framework/core/src/Forum/Content/Index.php new file mode 100644 index 000000000..77e0a1f15 --- /dev/null +++ b/framework/core/src/Forum/Content/Index.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Forum\Content; + +use Flarum\Api\Client; +use Flarum\Api\Controller\ListDiscussionsController; +use Flarum\Frontend\Content\ContentInterface; +use Flarum\Frontend\HtmlDocument; +use Flarum\User\User; +use Illuminate\Contracts\View\Factory; +use Psr\Http\Message\ServerRequestInterface as Request; + +class Index implements ContentInterface +{ + /** + * @var Client + */ + protected $api; + + /** + * @var Factory + */ + protected $view; + + /** + * @param Client $api + * @param Factory $view + */ + public function __construct(Client $api, Factory $view) + { + $this->api = $api; + $this->view = $view; + } + + /** + * {@inheritdoc} + */ + public function populate(HtmlDocument $document, Request $request) + { + $queryParams = $request->getQueryParams(); + + $sort = array_pull($queryParams, 'sort'); + $q = array_pull($queryParams, 'q'); + $page = array_pull($queryParams, 'page', 1); + + $sortMap = $this->getSortMap(); + + $params = [ + 'sort' => $sort && isset($sortMap[$sort]) ? $sortMap[$sort] : '', + 'filter' => compact('q'), + 'page' => ['offset' => ($page - 1) * 20, 'limit' => 20] + ]; + + $apiDocument = $this->getApiDocument($request->getAttribute('actor'), $params); + + $document->content = $this->view->make('flarum.forum::frontend.content.index', compact('apiDocument', 'page', 'forum')); + $document->payload['apiDocument'] = $apiDocument; + + return $document; + } + + /** + * Get a map of sort query param values and their API sort params. + * + * @return array + */ + private function getSortMap() + { + return [ + 'latest' => '-lastTime', + 'top' => '-commentsCount', + 'newest' => '-startTime', + 'oldest' => 'startTime' + ]; + } + + /** + * Get the result of an API request to list discussions. + * + * @param User $actor + * @param array $params + * @return object + */ + private function getApiDocument(User $actor, array $params) + { + return json_decode($this->api->send(ListDiscussionsController::class, $actor, $params)->getBody()); + } +} diff --git a/framework/core/src/Forum/Controller/AuthorizedWebAppController.php b/framework/core/src/Forum/Controller/AuthorizedWebAppController.php deleted file mode 100644 index 7f1a4890c..000000000 --- a/framework/core/src/Forum/Controller/AuthorizedWebAppController.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Forum\Controller; - -use Flarum\User\Exception\PermissionDeniedException; -use Psr\Http\Message\ServerRequestInterface as Request; - -class AuthorizedWebAppController extends FrontendController -{ - /** - * {@inheritdoc} - */ - public function render(Request $request) - { - if (! $request->getAttribute('session')->get('user_id')) { - throw new PermissionDeniedException; - } - - return parent::render($request); - } -} diff --git a/framework/core/src/Forum/Controller/FrontendController.php b/framework/core/src/Forum/Controller/FrontendController.php deleted file mode 100644 index ba863c019..000000000 --- a/framework/core/src/Forum/Controller/FrontendController.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Forum\Controller; - -use Flarum\Forum\Frontend; -use Flarum\Frontend\AbstractFrontendController; -use Illuminate\Contracts\Events\Dispatcher; - -class FrontendController extends AbstractFrontendController -{ - /** - * {@inheritdoc} - */ - public function __construct(Frontend $webApp, Dispatcher $events) - { - $this->webApp = $webApp; - $this->events = $events; - } -} diff --git a/framework/core/src/Forum/Controller/IndexController.php b/framework/core/src/Forum/Controller/IndexController.php deleted file mode 100644 index 028b716e7..000000000 --- a/framework/core/src/Forum/Controller/IndexController.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Forum\Controller; - -use Flarum\Api\Client as ApiClient; -use Flarum\Forum\Frontend; -use Flarum\User\User; -use Illuminate\Contracts\Events\Dispatcher; -use Psr\Http\Message\ServerRequestInterface as Request; - -class IndexController extends FrontendController -{ - /** - * @var ApiClient - */ - protected $api; - - /** - * A map of sort query param values to their API sort param. - * - * @var array - */ - private $sortMap = [ - 'latest' => '-lastTime', - 'top' => '-commentsCount', - 'newest' => '-startTime', - 'oldest' => 'startTime' - ]; - - /** - * {@inheritdoc} - */ - public function __construct(Frontend $webApp, Dispatcher $events, ApiClient $api) - { - parent::__construct($webApp, $events); - - $this->api = $api; - } - - /** - * {@inheritdoc} - */ - protected function getView(Request $request) - { - $view = parent::getView($request); - - $queryParams = $request->getQueryParams(); - - $sort = array_pull($queryParams, 'sort'); - $q = array_pull($queryParams, 'q'); - $page = array_pull($queryParams, 'page', 1); - - $params = [ - 'sort' => $sort && isset($this->sortMap[$sort]) ? $this->sortMap[$sort] : '', - 'filter' => compact('q'), - 'page' => ['offset' => ($page - 1) * 20, 'limit' => 20] - ]; - - $document = $this->getDocument($request->getAttribute('actor'), $params); - - $view->document = $document; - $view->content = app('view')->make('flarum.forum::frontend.content.index', compact('document', 'page', 'forum')); - - return $view; - } - - /** - * Get the result of an API request to list discussions. - * - * @param User $actor - * @param array $params - * @return object - */ - private function getDocument(User $actor, array $params) - { - return json_decode($this->api->send('Flarum\Api\Controller\ListDiscussionsController', $actor, $params)->getBody()); - } -} diff --git a/framework/core/src/Forum/ForumServiceProvider.php b/framework/core/src/Forum/ForumServiceProvider.php index 6e0c8a1c7..625ff636b 100644 --- a/framework/core/src/Forum/ForumServiceProvider.php +++ b/framework/core/src/Forum/ForumServiceProvider.php @@ -13,8 +13,6 @@ namespace Flarum\Forum; use Flarum\Event\ConfigureForumRoutes; use Flarum\Event\ConfigureMiddleware; -use Flarum\Extension\Event\Disabled; -use Flarum\Extension\Event\Enabled; use Flarum\Foundation\AbstractServiceProvider; use Flarum\Http\Middleware\AuthenticateWithSession; use Flarum\Http\Middleware\CollectGarbage; @@ -28,7 +26,6 @@ use Flarum\Http\Middleware\StartSession; use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; use Flarum\Http\UrlGenerator; -use Flarum\Settings\Event\Saved; use Flarum\Settings\SettingsRepositoryInterface; use Symfony\Component\Translation\TranslatorInterface; use Zend\Stratigility\MiddlewarePipe; @@ -69,6 +66,27 @@ class ForumServiceProvider extends AbstractServiceProvider return $pipe; }); + + $this->app->bind('flarum.forum.assets', function () { + $assets = $this->app->make('flarum.frontend.assets.defaults')('forum'); + + $assets->add(function () { + return [ + $this->app->make(Asset\FormatterJs::class), + $this->app->make(Asset\CustomCss::class) + ]; + }); + + return $assets; + }); + + $this->app->bind('flarum.forum.frontend', function () { + $view = $this->app->make('flarum.frontend.view.defaults')('forum'); + + $view->setAssets($this->app->make('flarum.forum.assets')); + + return $view; + }); } /** @@ -85,9 +103,13 @@ class ForumServiceProvider extends AbstractServiceProvider 'settings' => $this->app->make(SettingsRepositoryInterface::class) ]); - $this->flushWebAppAssetsWhenThemeChanged(); - - $this->flushWebAppAssetsWhenExtensionsChanged(); + $this->app->make('events')->subscribe( + new RecompileFrontendAssets( + $this->app->make('flarum.forum.assets'), + $this->app->make('flarum.locales'), + $this->app + ) + ); } /** @@ -111,7 +133,7 @@ class ForumServiceProvider extends AbstractServiceProvider if (isset($routes->getRouteData()[0]['GET'][$defaultRoute])) { $toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute]; } else { - $toDefaultController = $factory->toController(Controller\IndexController::class); + $toDefaultController = $factory->toForum(Content\Index::class); } $routes->get( @@ -120,34 +142,4 @@ class ForumServiceProvider extends AbstractServiceProvider $toDefaultController ); } - - protected function flushWebAppAssetsWhenThemeChanged() - { - $this->app->make('events')->listen(Saved::class, function (Saved $event) { - if (preg_match('/^theme_|^custom_less$/i', $event->key)) { - $this->getWebAppAssets()->flushCss(); - } - }); - } - - protected function flushWebAppAssetsWhenExtensionsChanged() - { - $events = $this->app->make('events'); - - $events->listen(Enabled::class, [$this, 'flushWebAppAssets']); - $events->listen(Disabled::class, [$this, 'flushWebAppAssets']); - } - - public function flushWebAppAssets() - { - $this->getWebAppAssets()->flush(); - } - - /** - * @return \Flarum\Frontend\FrontendAssets - */ - protected function getWebAppAssets() - { - return $this->app->make(Frontend::class)->getAssets(); - } } diff --git a/framework/core/src/Forum/Frontend.php b/framework/core/src/Forum/Frontend.php deleted file mode 100644 index 1f07298ce..000000000 --- a/framework/core/src/Forum/Frontend.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Forum; - -use Flarum\Formatter\Formatter; -use Flarum\Frontend\AbstractFrontend; -use Flarum\Frontend\FrontendAssetsFactory; -use Flarum\Frontend\FrontendViewFactory; -use Flarum\Locale\LocaleManager; -use Flarum\Settings\SettingsRepositoryInterface; - -class Frontend extends AbstractFrontend -{ - /** - * @var Formatter - */ - protected $formatter; - - /** - * {@inheritdoc} - */ - public function __construct( - FrontendAssetsFactory $assets, - FrontendViewFactory $view, - SettingsRepositoryInterface $settings, - LocaleManager $locales, - Formatter $formatter - ) { - parent::__construct($assets, $view, $settings, $locales); - - $this->formatter = $formatter; - } - - /** - * {@inheritdoc} - */ - public function getView() - { - $view = parent::getView(); - - $view->getJs()->addString(function () { - return $this->formatter->getJs(); - }); - - $view->getCss()->addString(function () { - return $this->settings->get('custom_less'); - }); - - return $view; - } - - /** - * {@inheritdoc} - */ - protected function getName() - { - return 'forum'; - } -} diff --git a/framework/core/src/Forum/RecompileFrontendAssets.php b/framework/core/src/Forum/RecompileFrontendAssets.php new file mode 100644 index 000000000..bf08d2fc5 --- /dev/null +++ b/framework/core/src/Forum/RecompileFrontendAssets.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Forum; + +use Flarum\Foundation\ValidationException; +use Flarum\Frontend\CompilerFactory; +use Flarum\Frontend\RecompileFrontendAssets as BaseListener; +use Flarum\Locale\LocaleManager; +use Flarum\Settings\Event\Saved; +use Flarum\Settings\Event\Saving; +use Flarum\Settings\OverrideSettingsRepository; +use Flarum\Settings\SettingsRepositoryInterface; +use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Filesystem\FilesystemAdapter; +use League\Flysystem\Adapter\NullAdapter; +use League\Flysystem\Filesystem; +use Less_Exception_Parser; + +class RecompileFrontendAssets extends BaseListener +{ + /** + * @var Container + */ + protected $container; + + /** + * @param CompilerFactory $assets + * @param LocaleManager $locales + * @param Container $container + */ + public function __construct(CompilerFactory $assets, LocaleManager $locales, Container $container) + { + parent::__construct($assets, $locales); + + $this->container = $container; + } + + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) + { + parent::subscribe($events); + + $events->listen(Saving::class, [$this, 'whenSettingsSaving']); + } + + /** + * @param Saving $event + * @throws ValidationException + */ + public function whenSettingsSaving(Saving $event) + { + if (isset($event->settings['custom_less'])) { + // We haven't saved the settings yet, but we want to trial a full + // recompile of the CSS to see if this custom LESS will break + // anything. In order to do that, we will temporarily override the + // settings repository with the new settings so that the recompile + // is effective. We will also use a dummy filesystem so that nothing + // is actually written yet. + + $settings = $this->container->make(SettingsRepositoryInterface::class); + + $this->container->extend( + SettingsRepositoryInterface::class, + function ($settings) use ($event) { + return new OverrideSettingsRepository($settings, $event->settings); + } + ); + + $assetsDir = $this->assets->getAssetsDir(); + $this->assets->setAssetsDir(new FilesystemAdapter(new Filesystem(new NullAdapter))); + + try { + $this->assets->makeCss()->commit(); + + foreach ($this->locales->getLocales() as $locale => $name) { + $this->assets->makeLocaleCss($locale)->commit(); + } + } catch (Less_Exception_Parser $e) { + throw new ValidationException(['custom_less' => $e->getMessage()]); + } + + $this->assets->setAssetsDir($assetsDir); + $this->container->instance(SettingsRepositoryInterface::class, $settings); + } + } + + /** + * @param Saved $event + */ + public function whenSettingsSaved(Saved $event) + { + parent::whenSettingsSaved($event); + + if (isset($event->settings['custom_less'])) { + $this->flushCss(); + } + } +} diff --git a/framework/core/src/Forum/routes.php b/framework/core/src/Forum/routes.php index 00f7bf303..f287ad13a 100644 --- a/framework/core/src/Forum/routes.php +++ b/framework/core/src/Forum/routes.php @@ -9,6 +9,7 @@ * file that was distributed with this source code. */ +use Flarum\Forum\Content; use Flarum\Forum\Controller; use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; @@ -17,31 +18,31 @@ return function (RouteCollection $map, RouteHandlerFactory $route) { $map->get( '/all', 'index', - $route->toController(Controller\IndexController::class) + $route->toForum(Content\Index::class) ); $map->get( '/d/{id:\d+(?:-[^/]*)?}[/{near:[^/]*}]', 'discussion', - $route->toController(Controller\DiscussionController::class) + $route->toForum(Content\Discussion::class) ); $map->get( '/u/{username}[/{filter:[^/]*}]', 'user', - $route->toController(Controller\FrontendController::class) + $route->toForum() ); $map->get( '/settings', 'settings', - $route->toController(Controller\AuthorizedWebAppController::class) + $route->toForum(Content\AssertRegistered::class) ); $map->get( '/notifications', 'notifications', - $route->toController(Controller\AuthorizedWebAppController::class) + $route->toForum(Content\AssertRegistered::class) ); $map->get( diff --git a/framework/core/src/Foundation/Console/CacheClearCommand.php b/framework/core/src/Foundation/Console/CacheClearCommand.php index 6bd6650da..a34884d65 100644 --- a/framework/core/src/Foundation/Console/CacheClearCommand.php +++ b/framework/core/src/Foundation/Console/CacheClearCommand.php @@ -11,10 +11,9 @@ namespace Flarum\Foundation\Console; -use Flarum\Admin\Frontend as AdminWebApp; use Flarum\Console\AbstractCommand; -use Flarum\Forum\Frontend as ForumWebApp; use Flarum\Foundation\Application; +use Flarum\Foundation\Event\ClearingCache; use Illuminate\Contracts\Cache\Store; class CacheClearCommand extends AbstractCommand @@ -24,16 +23,6 @@ class CacheClearCommand extends AbstractCommand */ protected $cache; - /** - * @var ForumWebApp - */ - protected $forum; - - /** - * @var AdminWebApp - */ - protected $admin; - /** * @var Application */ @@ -41,15 +30,11 @@ class CacheClearCommand extends AbstractCommand /** * @param Store $cache - * @param ForumWebApp $forum - * @param AdminWebApp $admin * @param Application $app */ - public function __construct(Store $cache, ForumWebApp $forum, AdminWebApp $admin, Application $app) + public function __construct(Store $cache, Application $app) { $this->cache = $cache; - $this->forum = $forum; - $this->admin = $admin; $this->app = $app; parent::__construct(); @@ -72,13 +57,12 @@ class CacheClearCommand extends AbstractCommand { $this->info('Clearing the cache...'); - $this->forum->getAssets()->flush(); - $this->admin->getAssets()->flush(); - $this->cache->flush(); $storagePath = $this->app->storagePath(); array_map('unlink', glob($storagePath.'/formatter/*')); array_map('unlink', glob($storagePath.'/locale/*')); + + event(new ClearingCache); } } diff --git a/framework/core/src/Event/ConfigureClientView.php b/framework/core/src/Foundation/Event/ClearingCache.php similarity index 63% rename from framework/core/src/Event/ConfigureClientView.php rename to framework/core/src/Foundation/Event/ClearingCache.php index 52375b9fe..fba314f82 100644 --- a/framework/core/src/Event/ConfigureClientView.php +++ b/framework/core/src/Foundation/Event/ClearingCache.php @@ -9,13 +9,8 @@ * file that was distributed with this source code. */ -namespace Flarum\Event; +namespace Flarum\Foundation\Event; -use Flarum\Frontend\Event\Rendering; - -/** - * @deprecated - */ -class ConfigureClientView extends Rendering +class ClearingCache { } diff --git a/framework/core/src/Foundation/Site.php b/framework/core/src/Foundation/Site.php index f0c3ca6e3..14a4f90da 100644 --- a/framework/core/src/Foundation/Site.php +++ b/framework/core/src/Foundation/Site.php @@ -20,6 +20,7 @@ use Flarum\Discussion\DiscussionServiceProvider; use Flarum\Extension\ExtensionServiceProvider; use Flarum\Formatter\FormatterServiceProvider; use Flarum\Forum\ForumServiceProvider; +use Flarum\Frontend\FrontendServiceProvider; use Flarum\Group\GroupServiceProvider; use Flarum\Locale\LocaleServiceProvider; use Flarum\Notification\NotificationServiceProvider; @@ -188,6 +189,7 @@ class Site $app->register(DiscussionServiceProvider::class); $app->register(FormatterServiceProvider::class); + $app->register(FrontendServiceProvider::class); $app->register(GroupServiceProvider::class); $app->register(NotificationServiceProvider::class); $app->register(PostServiceProvider::class); @@ -231,6 +233,11 @@ class Site 'default' => 'local', 'cloud' => 's3', 'disks' => [ + 'flarum-assets' => [ + 'driver' => 'local', + 'root' => $app->publicPath().'/assets', + 'url' => $app->url('assets') + ], 'flarum-avatars' => [ 'driver' => 'local', 'root' => $app->publicPath().'/assets/avatars' diff --git a/framework/core/src/Frontend/AbstractFrontend.php b/framework/core/src/Frontend/AbstractFrontend.php deleted file mode 100644 index 222ea9435..000000000 --- a/framework/core/src/Frontend/AbstractFrontend.php +++ /dev/null @@ -1,184 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend; - -use Flarum\Locale\LocaleManager; -use Flarum\Settings\SettingsRepositoryInterface; - -abstract class AbstractFrontend -{ - /** - * @var FrontendAssetsFactory - */ - protected $assets; - - /** - * @var FrontendViewFactory - */ - protected $view; - - /** - * @var SettingsRepositoryInterface - */ - protected $settings; - - /** - * @var LocaleManager - */ - protected $locales; - - /** - * @param FrontendAssetsFactory $assets - * @param FrontendViewFactory $view - * @param SettingsRepositoryInterface $settings - * @param LocaleManager $locales - */ - public function __construct(FrontendAssetsFactory $assets, FrontendViewFactory $view, SettingsRepositoryInterface $settings, LocaleManager $locales) - { - $this->assets = $assets; - $this->view = $view; - $this->settings = $settings; - $this->locales = $locales; - } - - /** - * @return FrontendView - */ - public function getView() - { - $view = $this->view->make($this->getLayout(), $this->getAssets()); - - $this->addDefaultAssets($view); - $this->addCustomLess($view); - $this->addTranslations($view); - - return $view; - } - - /** - * @return FrontendAssets - */ - public function getAssets() - { - return $this->assets->make($this->getName()); - } - - /** - * Get the name of the client. - * - * @return string - */ - abstract protected function getName(); - - /** - * Get the path to the client layout view. - * - * @return string - */ - protected function getLayout() - { - return 'flarum.forum::frontend.'.$this->getName(); - } - - /** - * Get a regular expression to match against translation keys. - * - * @return string - */ - protected function getTranslationFilter() - { - return '/^.+(?:\.|::)(?:'.$this->getName().'|lib)\./'; - } - - /** - * @param FrontendView $view - */ - private function addDefaultAssets(FrontendView $view) - { - $root = __DIR__.'/../..'; - $name = $this->getName(); - - $view->getJs()->addFile("$root/js/dist/$name.js"); - $view->getCss()->addFile("$root/less/$name.less"); - } - - /** - * @param FrontendView $view - */ - private function addCustomLess(FrontendView $view) - { - $css = $view->getCss(); - $localeCss = $view->getLocaleCss(); - - $lessVariables = function () { - $less = ''; - - foreach ($this->getLessVariables() as $name => $value) { - $less .= "@$name: $value;"; - } - - return $less; - }; - - $css->addString($lessVariables); - $localeCss->addString($lessVariables); - } - - /** - * Get the values of any LESS variables to compile into the CSS, based on - * the forum's configuration. - * - * @return array - */ - private function getLessVariables() - { - return [ - 'config-primary-color' => $this->settings->get('theme_primary_color') ?: '#000', - 'config-secondary-color' => $this->settings->get('theme_secondary_color') ?: '#000', - 'config-dark-mode' => $this->settings->get('theme_dark_mode') ? 'true' : 'false', - 'config-colored-header' => $this->settings->get('theme_colored_header') ? 'true' : 'false' - ]; - } - - /** - * @param FrontendView $view - */ - private function addTranslations(FrontendView $view) - { - $translations = array_get($this->locales->getTranslator()->getCatalogue()->all(), 'messages', []); - - $translations = $this->filterTranslations($translations); - - $view->getLocaleJs()->setTranslations($translations); - } - - /** - * Take a selection of keys from a collection of translations. - * - * @param array $translations - * @return array - */ - private function filterTranslations(array $translations) - { - $filter = $this->getTranslationFilter(); - - if (! $filter) { - return []; - } - - $filtered = array_filter(array_keys($translations), function ($id) use ($filter) { - return preg_match($filter, $id); - }); - - return array_only($translations, $filtered); - } -} diff --git a/framework/core/src/Frontend/AbstractFrontendController.php b/framework/core/src/Frontend/AbstractFrontendController.php deleted file mode 100644 index c924392dc..000000000 --- a/framework/core/src/Frontend/AbstractFrontendController.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend; - -use Flarum\Event\ConfigureClientView; -use Flarum\Frontend\Event\Rendering; -use Flarum\Http\Controller\AbstractHtmlController; -use Illuminate\Contracts\Events\Dispatcher; -use Psr\Http\Message\ServerRequestInterface as Request; - -abstract class AbstractFrontendController extends AbstractHtmlController -{ - /** - * @var AbstractFrontend - */ - protected $webApp; - - /** - * @var Dispatcher - */ - protected $events; - - /** - * {@inheritdoc} - */ - public function render(Request $request) - { - $view = $this->getView($request); - - $this->events->dispatch( - new ConfigureClientView($this, $view, $request) - ); - $this->events->dispatch( - new Rendering($this, $view, $request) - ); - - return $view->render($request); - } - - /** - * @param Request $request - * @return \Flarum\Frontend\FrontendView - */ - protected function getView(Request $request) - { - return $this->webApp->getView(); - } -} diff --git a/framework/core/src/Frontend/Asset/AssetInterface.php b/framework/core/src/Frontend/Asset/AssetInterface.php new file mode 100644 index 000000000..67c4f040f --- /dev/null +++ b/framework/core/src/Frontend/Asset/AssetInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Asset; + +use Flarum\Frontend\Compiler\Source\SourceCollector; + +interface AssetInterface +{ + /** + * @param SourceCollector $sources + */ + public function js(SourceCollector $sources); + + /** + * @param SourceCollector $sources + */ + public function css(SourceCollector $sources); + + /** + * @param SourceCollector $sources + * @param string $locale + */ + public function localeJs(SourceCollector $sources, string $locale); + + /** + * @param SourceCollector $sources + * @param string $locale + */ + public function localeCss(SourceCollector $sources, string $locale); +} diff --git a/framework/core/src/Frontend/Asset/CoreAssets.php b/framework/core/src/Frontend/Asset/CoreAssets.php new file mode 100644 index 000000000..9aaff9539 --- /dev/null +++ b/framework/core/src/Frontend/Asset/CoreAssets.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Asset; + +use Flarum\Frontend\Compiler\Source\SourceCollector; + +class CoreAssets implements AssetInterface +{ + /** + * @var string + */ + protected $name; + + /** + * @param string $name + */ + public function __construct(string $name) + { + $this->name = $name; + } + + public function js(SourceCollector $sources) + { + $sources->addFile(__DIR__."/../../../js/dist/$this->name.js"); + } + + public function css(SourceCollector $sources) + { + $sources->addFile(__DIR__."/../../../less/$this->name.less"); + } + + public function localeJs(SourceCollector $sources, string $locale) + { + } + + public function localeCss(SourceCollector $sources, string $locale) + { + } +} diff --git a/framework/core/src/Frontend/Asset/ExtensionAssets.php b/framework/core/src/Frontend/Asset/ExtensionAssets.php new file mode 100644 index 000000000..6c1206d11 --- /dev/null +++ b/framework/core/src/Frontend/Asset/ExtensionAssets.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Asset; + +use Flarum\Extension\Extension; +use Flarum\Frontend\Compiler\Source\SourceCollector; + +class ExtensionAssets implements AssetInterface +{ + /** + * @var Extension + */ + protected $extension; + + /** + * @var array + */ + protected $css; + + /** + * @var string|callable|null + */ + protected $js; + + /** + * @param Extension $extension + * @param array $css + * @param string|callable|null $js + */ + public function __construct(Extension $extension, array $css, $js = null) + { + $this->extension = $extension; + $this->css = $css; + $this->js = $js; + } + + public function js(SourceCollector $sources) + { + if ($this->js) { + $sources->addString(function () { + $name = $this->extension->getId(); + + return 'var module={};'.$this->getContent($this->js).";flarum.extensions['$name']=module.exports"; + }); + } + } + + public function css(SourceCollector $sources) + { + foreach ($this->css as $asset) { + if (is_callable($asset)) { + $sources->addString($asset); + } else { + $sources->addFile($asset); + } + } + } + + private function getContent($asset) + { + return is_callable($asset) ? $asset() : file_get_contents($asset); + } + + public function localeJs(SourceCollector $sources, string $locale) + { + } + + public function localeCss(SourceCollector $sources, string $locale) + { + } +} diff --git a/framework/core/src/Frontend/Asset/JsCompiler.php b/framework/core/src/Frontend/Asset/JsCompiler.php deleted file mode 100644 index bb432aecf..000000000 --- a/framework/core/src/Frontend/Asset/JsCompiler.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend\Asset; - -use Illuminate\Cache\Repository; - -class JsCompiler extends RevisionCompiler -{ - /** - * @var Repository - */ - protected $cache; - - /** - * @param string $path - * @param string $filename - * @param bool $watch - * @param Repository $cache - */ - public function __construct($path, $filename, $watch = false, Repository $cache = null) - { - parent::__construct($path, $filename, $watch); - - $this->cache = $cache; - } - - /** - * {@inheritdoc} - */ - protected function format($string) - { - return $string.";\n"; - } - - /** - * {@inheritdoc} - */ - protected function getCacheDifferentiator() - { - return $this->watch; - } -} diff --git a/framework/core/src/Frontend/Asset/LessCompiler.php b/framework/core/src/Frontend/Asset/LessCompiler.php deleted file mode 100644 index 4a5990778..000000000 --- a/framework/core/src/Frontend/Asset/LessCompiler.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend\Asset; - -use Less_Exception_Parser; -use Less_Parser; - -class LessCompiler extends RevisionCompiler -{ - /** - * @var string - */ - protected $cachePath; - - /** - * @param string $path - * @param string $filename - * @param bool $watch - * @param string $cachePath - */ - public function __construct($path, $filename, $watch, $cachePath) - { - parent::__construct($path, $filename, $watch); - - $this->cachePath = $cachePath; - } - - /** - * {@inheritdoc} - */ - public function compile() - { - if (! count($this->files) || ! count($this->strings)) { - return; - } - - ini_set('xdebug.max_nesting_level', 200); - - $parser = new Less_Parser([ - 'compress' => true, - 'cache_dir' => $this->cachePath, - 'import_dirs' => [ - base_path('vendor/components/font-awesome/less') => '', - ], - ]); - - try { - foreach ($this->files as $file) { - $parser->parseFile($file); - } - - foreach ($this->strings as $callback) { - $parser->parse($callback()); - } - - return $parser->getCss(); - } catch (Less_Exception_Parser $e) { - // TODO: log an error somewhere? - } - } -} diff --git a/framework/core/src/Frontend/Asset/LessVariables.php b/framework/core/src/Frontend/Asset/LessVariables.php new file mode 100644 index 000000000..9e2418ac3 --- /dev/null +++ b/framework/core/src/Frontend/Asset/LessVariables.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Asset; + +use Flarum\Frontend\Compiler\Source\SourceCollector; +use Flarum\Settings\SettingsRepositoryInterface; + +class LessVariables implements AssetInterface +{ + /** + * @var SettingsRepositoryInterface + */ + protected $settings; + + /** + * @param SettingsRepositoryInterface $settings + */ + public function __construct(SettingsRepositoryInterface $settings) + { + $this->settings = $settings; + } + + public function css(SourceCollector $sources) + { + $this->addLessVariables($sources); + } + + public function localeCss(SourceCollector $sources, string $locale) + { + $this->addLessVariables($sources); + } + + private function addLessVariables(SourceCollector $compiler) + { + $vars = [ + 'config-primary-color' => $this->settings->get('theme_primary_color', '#000'), + 'config-secondary-color' => $this->settings->get('theme_secondary_color', '#000'), + 'config-dark-mode' => $this->settings->get('theme_dark_mode') ? 'true' : 'false', + 'config-colored-header' => $this->settings->get('theme_colored_header') ? 'true' : 'false' + ]; + + $compiler->addString(function () use ($vars) { + return array_reduce(array_keys($vars), function ($string, $name) use ($vars) { + return $string."@$name: {$vars[$name]};"; + }, ''); + }); + } + + public function js(SourceCollector $sources) + { + } + + public function localeJs(SourceCollector $sources, string $locale) + { + } +} diff --git a/framework/core/src/Frontend/Asset/LocaleAssets.php b/framework/core/src/Frontend/Asset/LocaleAssets.php new file mode 100644 index 000000000..9e5fa7701 --- /dev/null +++ b/framework/core/src/Frontend/Asset/LocaleAssets.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Asset; + +use Flarum\Frontend\Compiler\Source\SourceCollector; +use Flarum\Locale\LocaleManager; + +class LocaleAssets implements AssetInterface +{ + /** + * @var LocaleManager + */ + protected $locales; + + /** + * @param LocaleManager $locales + */ + public function __construct(LocaleManager $locales) + { + $this->locales = $locales; + } + + public function localeJs(SourceCollector $sources, string $locale) + { + foreach ($this->locales->getJsFiles($locale) as $file) { + $sources->addFile($file); + } + } + + public function localeCss(SourceCollector $sources, string $locale) + { + foreach ($this->locales->getCssFiles($locale) as $file) { + $sources->addFile($file); + } + } + + public function js(SourceCollector $sources) + { + } + + public function css(SourceCollector $sources) + { + } +} diff --git a/framework/core/src/Frontend/Asset/LocaleJsCompiler.php b/framework/core/src/Frontend/Asset/LocaleJsCompiler.php deleted file mode 100644 index f05a74628..000000000 --- a/framework/core/src/Frontend/Asset/LocaleJsCompiler.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend\Asset; - -class LocaleJsCompiler extends JsCompiler -{ - protected $translations = []; - - public function setTranslations(array $translations) - { - $this->translations = $translations; - } - - public function compile() - { - $output = 'flarum.core.app.translator.translations='.json_encode($this->translations).";\n"; - - foreach ($this->files as $filename) { - $output .= file_get_contents($filename); - } - - return $this->format($output); - } -} diff --git a/framework/core/src/Frontend/Asset/RevisionCompiler.php b/framework/core/src/Frontend/Asset/RevisionCompiler.php deleted file mode 100644 index 18889a905..000000000 --- a/framework/core/src/Frontend/Asset/RevisionCompiler.php +++ /dev/null @@ -1,207 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend\Asset; - -class RevisionCompiler implements CompilerInterface -{ - /** - * @var string[] - */ - protected $files = []; - - /** - * @var callable[] - */ - protected $strings = []; - - /** - * @var bool - */ - protected $watch; - - /** - * @param string $path - * @param string $filename - * @param bool $watch - */ - public function __construct($path, $filename, $watch = false) - { - $this->path = $path; - $this->filename = $filename; - $this->watch = $watch; - } - - /** - * {@inheritdoc} - */ - public function setFilename($filename) - { - $this->filename = $filename; - } - - /** - * {@inheritdoc} - */ - public function addFile($file) - { - $this->files[] = $file; - } - - /** - * {@inheritdoc} - */ - public function addString(callable $callback) - { - $this->strings[] = $callback; - } - - /** - * {@inheritdoc} - */ - public function getFile() - { - $old = $current = $this->getRevision(); - - $ext = pathinfo($this->filename, PATHINFO_EXTENSION); - $file = $this->path.'/'.substr_replace($this->filename, '-'.$old, -strlen($ext) - 1, 0); - - if ($this->watch || ! $old) { - $cacheDifferentiator = [$this->getCacheDifferentiator()]; - - foreach ($this->files as $source) { - $cacheDifferentiator[] = [$source, filemtime($source)]; - } - - foreach ($this->strings as $callback) { - $cacheDifferentiator[] = $callback(); - } - - $current = hash('crc32b', serialize($cacheDifferentiator)); - } - - $exists = file_exists($file); - - if (! $exists || $old !== $current) { - if ($exists) { - unlink($file); - } - - $file = $this->path.'/'.substr_replace($this->filename, '-'.$current, -strlen($ext) - 1, 0); - - if ($content = $this->compile()) { - $this->putRevision($current); - - file_put_contents($file, $content); - } else { - return; - } - } - - return $file; - } - - /** - * @return mixed - */ - protected function getCacheDifferentiator() - { - } - - /** - * @param string $string - * @return string - */ - protected function format($string) - { - return $string; - } - - /** - * {@inheritdoc} - */ - public function compile() - { - $output = ''; - - foreach ($this->files as $file) { - $output .= $this->formatFile($file); - } - - foreach ($this->strings as $callback) { - $output .= $this->format($callback()); - } - - return $output; - } - - /** - * @param string $file - * @return string - */ - protected function formatFile($file) - { - return $this->format(file_get_contents($file)); - } - - /** - * @return string - */ - protected function getRevisionFile() - { - return $this->path.'/rev-manifest.json'; - } - - /** - * @return string|null - */ - protected function getRevision() - { - if (file_exists($file = $this->getRevisionFile())) { - $manifest = json_decode(file_get_contents($file), true); - - return array_get($manifest, $this->filename); - } - } - - /** - * @param string $revision - * @return int - */ - protected function putRevision($revision) - { - if (file_exists($file = $this->getRevisionFile())) { - $manifest = json_decode(file_get_contents($file), true); - } else { - $manifest = []; - } - - $manifest[$this->filename] = $revision; - - return file_put_contents($this->getRevisionFile(), json_encode($manifest)); - } - - /** - * {@inheritdoc} - */ - public function flush() - { - $revision = $this->getRevision(); - - $ext = pathinfo($this->filename, PATHINFO_EXTENSION); - - $file = $this->path.'/'.substr_replace($this->filename, '-'.$revision, -strlen($ext) - 1, 0); - - if (file_exists($file)) { - unlink($file); - } - } -} diff --git a/framework/core/src/Frontend/Asset/Translations.php b/framework/core/src/Frontend/Asset/Translations.php new file mode 100644 index 000000000..6e53b842f --- /dev/null +++ b/framework/core/src/Frontend/Asset/Translations.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Asset; + +use Flarum\Frontend\Compiler\Source\SourceCollector; +use Flarum\Locale\LocaleManager; + +class Translations implements AssetInterface +{ + /** + * @var LocaleManager + */ + protected $locales; + + /** + * @var callable + */ + protected $filter; + + /** + * @param LocaleManager $locales + */ + public function __construct(LocaleManager $locales) + { + $this->locales = $locales; + + $this->filter = function () { + return false; + }; + } + + public function localeJs(SourceCollector $sources, string $locale) + { + $sources->addString(function () use ($locale) { + $translations = $this->getTranslations($locale); + + return 'flarum.core.app.translator.addTranslations('.json_encode($translations).')'; + }); + } + + private function getTranslations(string $locale) + { + $translations = $this->locales->getTranslator()->getCatalogue($locale)->all('messages'); + + return array_only( + $translations, + array_filter(array_keys($translations), $this->filter) + ); + } + + /** + * @return callable + */ + public function getFilter(): callable + { + return $this->filter; + } + + /** + * @param callable $filter + */ + public function setFilter(callable $filter) + { + $this->filter = $filter; + } + + public function js(SourceCollector $sources) + { + } + + public function css(SourceCollector $sources) + { + } + + public function localeCss(SourceCollector $sources, string $locale) + { + } +} diff --git a/framework/core/src/Frontend/Asset/CompilerInterface.php b/framework/core/src/Frontend/Compiler/CompilerInterface.php similarity index 60% rename from framework/core/src/Frontend/Asset/CompilerInterface.php rename to framework/core/src/Frontend/Compiler/CompilerInterface.php index 2fb113ab5..39a66dedc 100644 --- a/framework/core/src/Frontend/Asset/CompilerInterface.php +++ b/framework/core/src/Frontend/Compiler/CompilerInterface.php @@ -9,34 +9,31 @@ * file that was distributed with this source code. */ -namespace Flarum\Frontend\Asset; +namespace Flarum\Frontend\Compiler; interface CompilerInterface { /** - * @param string $filename + * @return string */ - public function setFilename($filename); + public function getFilename(): string; /** - * @param string $file + * @param string $filename */ - public function addFile($file); + public function setFilename(string $filename); /** * @param callable $callback */ - public function addString(callable $callback); + public function addSources(callable $callback); + + public function commit(); /** - * @return string + * @return string|null */ - public function getFile(); - - /** - * @return string - */ - public function compile(); + public function getUrl(): ?string; public function flush(); } diff --git a/framework/core/src/Frontend/Compiler/JsCompiler.php b/framework/core/src/Frontend/Compiler/JsCompiler.php new file mode 100644 index 000000000..9bf5719e6 --- /dev/null +++ b/framework/core/src/Frontend/Compiler/JsCompiler.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Compiler; + +use axy\sourcemap\SourceMap; +use Flarum\Frontend\Compiler\Source\FileSource; + +class JsCompiler extends RevisionCompiler +{ + /** + * {@inheritdoc} + */ + protected function save(string $file, array $sources): bool + { + $mapFile = $file.'.map'; + + $map = new SourceMap(); + $map->file = $mapFile; + $output = []; + $line = 0; + + // For each of the sources, get their content and add it to the + // output. For file sources, if a sourcemap is present, add it to + // the output sourcemap. + foreach ($sources as $source) { + $content = $source->getContent(); + + if ($source instanceof FileSource) { + $sourceMap = $source->getPath().'.map'; + + if (file_exists($sourceMap)) { + $map->concat($sourceMap, $line); + } + } + + $content = $this->format($content); + $output[] = $content; + $line += substr_count($content, "\n") + 1; + } + + // Add a comment to the end of our file to point to the sourcemap + // we just constructed. We will then write the JS file, save the + // map to a temporary location, and then move it to the asset dir. + $output[] = '//# sourceMappingURL='.$this->assetsDir->url($mapFile); + + $this->assetsDir->put($file, implode("\n", $output)); + + $mapTemp = tempnam(sys_get_temp_dir(), $mapFile); + $map->save($mapTemp); + $this->assetsDir->put($mapFile, file_get_contents($mapTemp)); + @unlink($mapTemp); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function format(string $string): string + { + return preg_replace('~//# sourceMappingURL.*$~s', '', $string).";\n"; + } + + /** + * {@inheritdoc} + */ + protected function delete(string $file) + { + parent::delete($file); + + if ($this->assetsDir->has($mapFile = $file.'.map')) { + $this->assetsDir->delete($mapFile); + } + } +} diff --git a/framework/core/src/Frontend/Compiler/LessCompiler.php b/framework/core/src/Frontend/Compiler/LessCompiler.php new file mode 100644 index 000000000..947c2b698 --- /dev/null +++ b/framework/core/src/Frontend/Compiler/LessCompiler.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Compiler; + +use Flarum\Frontend\Compiler\Source\FileSource; +use Less_Parser; + +class LessCompiler extends RevisionCompiler +{ + /** + * @var string + */ + protected $cacheDir; + + /** + * @var array + */ + protected $importDirs = []; + + /** + * @return string + */ + public function getCacheDir(): string + { + return $this->cacheDir; + } + + /** + * @param string $cacheDir + */ + public function setCacheDir(string $cacheDir) + { + $this->cacheDir = $cacheDir; + } + + /** + * @return array + */ + public function getImportDirs(): array + { + return $this->importDirs; + } + + /** + * @param array $importDirs + */ + public function setImportDirs(array $importDirs) + { + $this->importDirs = $importDirs; + } + + /** + * {@inheritdoc} + */ + protected function compile(array $sources): string + { + if (! count($sources)) { + return ''; + } + + ini_set('xdebug.max_nesting_level', 200); + + $parser = new Less_Parser([ + 'compress' => true, + 'cache_dir' => $this->cacheDir, + 'import_dirs' => $this->importDirs + ]); + + foreach ($sources as $source) { + if ($source instanceof FileSource) { + $parser->parseFile($source->getPath()); + } else { + $parser->parse($source->getContent()); + } + } + + return $parser->getCss(); + } + + /** + * @return mixed + */ + protected function getCacheDifferentiator() + { + return time(); + } +} diff --git a/framework/core/src/Frontend/Compiler/RevisionCompiler.php b/framework/core/src/Frontend/Compiler/RevisionCompiler.php new file mode 100644 index 000000000..f126b7cea --- /dev/null +++ b/framework/core/src/Frontend/Compiler/RevisionCompiler.php @@ -0,0 +1,276 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Compiler; + +use Flarum\Frontend\Compiler\Source\SourceCollector; +use Flarum\Frontend\Compiler\Source\SourceInterface; +use Illuminate\Filesystem\FilesystemAdapter; + +class RevisionCompiler implements CompilerInterface +{ + const REV_MANIFEST = 'rev-manifest.json'; + + const EMPTY_REVISION = 'empty'; + + /** + * @var FilesystemAdapter + */ + protected $assetsDir; + + /** + * @var string + */ + protected $filename; + + /** + * @var callable[] + */ + protected $sourcesCallbacks = []; + + /** + * @param FilesystemAdapter $assetsDir + * @param string $filename + */ + public function __construct(FilesystemAdapter $assetsDir, string $filename) + { + $this->assetsDir = $assetsDir; + $this->filename = $filename; + } + + /** + * {@inheritdoc} + */ + public function getFilename(): string + { + return $this->filename; + } + + /** + * {@inheritdoc} + */ + public function setFilename(string $filename) + { + $this->filename = $filename; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + $sources = $this->getSources(); + + $oldRevision = $this->getRevision(); + + $newRevision = $this->calculateRevision($sources); + + $oldFile = $oldRevision ? $this->getFilenameForRevision($oldRevision) : null; + + if ($oldRevision !== $newRevision || ($oldFile && ! $this->assetsDir->has($oldFile))) { + $newFile = $this->getFilenameForRevision($newRevision); + + if (! $this->save($newFile, $sources)) { + // If no file was written (because the sources were empty), we + // will set the revision to a special value so that we can tell + // that this file does not have a URL. + $newRevision = static::EMPTY_REVISION; + } + + $this->putRevision($newRevision); + + if ($oldFile) { + $this->delete($oldFile); + } + } + } + + /** + * {@inheritdoc} + */ + public function addSources(callable $callback) + { + $this->sourcesCallbacks[] = $callback; + } + + /** + * @return SourceInterface[] + */ + protected function getSources() + { + $sources = new SourceCollector; + + foreach ($this->sourcesCallbacks as $callback) { + $callback($sources); + } + + return $sources->getSources(); + } + + /** + * {@inheritdoc} + */ + public function getUrl(): ?string + { + $revision = $this->getRevision(); + + if (! $revision) { + $this->commit(); + + $revision = $this->getRevision(); + + if (! $revision) { + return null; + } + } + + if ($revision === static::EMPTY_REVISION) { + return null; + } + + $file = $this->getFilenameForRevision($revision); + + return $this->assetsDir->url($file); + } + + /** + * @param string $file + * @param SourceInterface[] $sources + * @return bool true if the file was written, false if there was nothing to write + */ + protected function save(string $file, array $sources): bool + { + if ($content = $this->compile($sources)) { + $this->assetsDir->put($file, $content); + + return true; + } + + return false; + } + + /** + * @param SourceInterface[] $sources + * @return string + */ + protected function compile(array $sources): string + { + $output = ''; + + foreach ($sources as $source) { + $output .= $this->format($source->getContent()); + } + + return $output; + } + + /** + * @param string $string + * @return string + */ + protected function format(string $string): string + { + return $string; + } + + /** + * Get the filename for the given revision. + * + * @param string $revision + * @return string + */ + protected function getFilenameForRevision(string $revision): string + { + $ext = pathinfo($this->filename, PATHINFO_EXTENSION); + + return substr_replace($this->filename, '-'.$revision, -strlen($ext) - 1, 0); + } + + /** + * @return string|null + */ + protected function getRevision(): ?string + { + if ($this->assetsDir->has(static::REV_MANIFEST)) { + $manifest = json_decode($this->assetsDir->read(static::REV_MANIFEST), true); + + return array_get($manifest, $this->filename); + } + + return null; + } + + /** + * @param string|null $revision + */ + protected function putRevision(?string $revision) + { + if ($this->assetsDir->has(static::REV_MANIFEST)) { + $manifest = json_decode($this->assetsDir->read(static::REV_MANIFEST), true); + } else { + $manifest = []; + } + + if ($revision) { + $manifest[$this->filename] = $revision; + } else { + unset($manifest[$this->filename]); + } + + $this->assetsDir->put(static::REV_MANIFEST, json_encode($manifest)); + } + + /** + * @param SourceInterface[] $sources + * @return string + */ + protected function calculateRevision(array $sources): string + { + $cacheDifferentiator = [$this->getCacheDifferentiator()]; + + foreach ($sources as $source) { + $cacheDifferentiator[] = $source->getCacheDifferentiator(); + } + + return hash('crc32b', serialize($cacheDifferentiator)); + } + + /** + * @return mixed + */ + protected function getCacheDifferentiator() + { + } + + /** + * {@inheritdoc} + */ + public function flush() + { + if ($revision = $this->getRevision()) { + $file = $this->getFilenameForRevision($revision); + + $this->delete($file); + + $this->putRevision(null); + } + } + + /** + * @param string $file + */ + protected function delete(string $file) + { + if ($this->assetsDir->has($file)) { + $this->assetsDir->delete($file); + } + } +} diff --git a/framework/core/src/Frontend/Compiler/Source/FileSource.php b/framework/core/src/Frontend/Compiler/Source/FileSource.php new file mode 100644 index 000000000..cc5056b60 --- /dev/null +++ b/framework/core/src/Frontend/Compiler/Source/FileSource.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Compiler\Source; + +class FileSource implements SourceInterface +{ + /** + * @var string + */ + protected $path; + + /** + * @param string $path + */ + public function __construct(string $path) + { + $this->path = $path; + } + + /** + * @return string + */ + public function getContent(): string + { + return file_get_contents($this->path); + } + + /** + * @return mixed + */ + public function getCacheDifferentiator() + { + return [$this->path, filemtime($this->path)]; + } + + /** + * @return string + */ + public function getPath(): string + { + return $this->path; + } +} diff --git a/framework/core/src/Frontend/Compiler/Source/SourceCollector.php b/framework/core/src/Frontend/Compiler/Source/SourceCollector.php new file mode 100644 index 000000000..6b08c6064 --- /dev/null +++ b/framework/core/src/Frontend/Compiler/Source/SourceCollector.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Compiler\Source; + +class SourceCollector +{ + /** + * @var SourceInterface[] + */ + protected $sources = []; + + /** + * @param string $file + * @return $this + */ + public function addFile(string $file) + { + $this->sources[] = new FileSource($file); + + return $this; + } + + /** + * @param callable $callback + * @return $this + */ + public function addString(callable $callback) + { + $this->sources[] = new StringSource($callback); + + return $this; + } + + /** + * @return SourceInterface[] + */ + public function getSources() + { + return $this->sources; + } +} diff --git a/framework/core/src/Frontend/Compiler/Source/SourceInterface.php b/framework/core/src/Frontend/Compiler/Source/SourceInterface.php new file mode 100644 index 000000000..71ea52c4e --- /dev/null +++ b/framework/core/src/Frontend/Compiler/Source/SourceInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Compiler\Source; + +interface SourceInterface +{ + /** + * @return string + */ + public function getContent(): string; + + /** + * @return mixed + */ + public function getCacheDifferentiator(); +} diff --git a/framework/core/src/Frontend/Compiler/Source/StringSource.php b/framework/core/src/Frontend/Compiler/Source/StringSource.php new file mode 100644 index 000000000..fdbb481bc --- /dev/null +++ b/framework/core/src/Frontend/Compiler/Source/StringSource.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Compiler\Source; + +class StringSource implements SourceInterface +{ + /** + * @var callable + */ + protected $callback; + + private $content; + + /** + * @param callable $callback + */ + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + /** + * @return string + */ + public function getContent(): string + { + if (is_null($this->content)) { + $this->content = call_user_func($this->callback); + } + + return $this->content; + } + + /** + * @return mixed + */ + public function getCacheDifferentiator() + { + return $this->getContent(); + } +} diff --git a/framework/core/src/Frontend/CompilerFactory.php b/framework/core/src/Frontend/CompilerFactory.php new file mode 100644 index 000000000..fc541e5a8 --- /dev/null +++ b/framework/core/src/Frontend/CompilerFactory.php @@ -0,0 +1,239 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend; + +use Flarum\Frontend\Asset\AssetInterface; +use Flarum\Frontend\Compiler\CompilerInterface; +use Flarum\Frontend\Compiler\JsCompiler; +use Flarum\Frontend\Compiler\LessCompiler; +use Flarum\Frontend\Compiler\Source\SourceCollector; +use Illuminate\Filesystem\FilesystemAdapter; + +/** + * A factory class for creating frontend asset compilers. + */ +class CompilerFactory +{ + /** + * @var string + */ + protected $name; + + /** + * @var FilesystemAdapter + */ + protected $assetsDir; + + /** + * @var string + */ + protected $cacheDir; + + /** + * @var array + */ + protected $lessImportDirs; + + /** + * @var AssetInterface[] + */ + protected $assets = []; + + /** + * @var callable[] + */ + protected $addCallbacks = []; + + /** + * @param string $name + * @param FilesystemAdapter $assetsDir + * @param string $cacheDir + * @param array|null $lessImportDirs + */ + public function __construct(string $name, FilesystemAdapter $assetsDir, string $cacheDir = null, array $lessImportDirs = null) + { + $this->name = $name; + $this->assetsDir = $assetsDir; + $this->cacheDir = $cacheDir; + $this->lessImportDirs = $lessImportDirs; + } + + /** + * @param callable $callback + */ + public function add(callable $callback) + { + $this->addCallbacks[] = $callback; + } + + /** + * @return JsCompiler + */ + public function makeJs(): JsCompiler + { + $compiler = new JsCompiler($this->assetsDir, $this->name.'.js'); + + $this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) { + $asset->js($sources); + }); + + return $compiler; + } + + /** + * @return LessCompiler + */ + public function makeCss(): LessCompiler + { + $compiler = $this->makeLessCompiler($this->name.'.css'); + + $this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) { + $asset->css($sources); + }); + + return $compiler; + } + + /** + * @param string $locale + * @return JsCompiler + */ + public function makeLocaleJs(string $locale): JsCompiler + { + $compiler = new JsCompiler($this->assetsDir, $this->name.'-'.$locale.'.js'); + + $this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) use ($locale) { + $asset->localeJs($sources, $locale); + }); + + return $compiler; + } + + /** + * @param string $locale + * @return LessCompiler + */ + public function makeLocaleCss(string $locale): LessCompiler + { + $compiler = $this->makeLessCompiler($this->name.'-'.$locale.'.css'); + + $this->addSources($compiler, function (AssetInterface $asset, SourceCollector $sources) use ($locale) { + $asset->localeCss($sources, $locale); + }); + + return $compiler; + } + + /** + * @param string $filename + * @return LessCompiler + */ + protected function makeLessCompiler(string $filename): LessCompiler + { + $compiler = new LessCompiler($this->assetsDir, $filename); + + if ($this->cacheDir) { + $compiler->setCacheDir($this->cacheDir.'/less'); + } + + if ($this->lessImportDirs) { + $compiler->setImportDirs($this->lessImportDirs); + } + + return $compiler; + } + + protected function fireAddCallbacks() + { + foreach ($this->addCallbacks as $callback) { + $assets = $callback($this); + $this->assets = array_merge($this->assets, is_array($assets) ? $assets : [$assets]); + } + + $this->addCallbacks = []; + } + + private function addSources(CompilerInterface $compiler, callable $callback) + { + $compiler->addSources(function ($sources) use ($callback) { + $this->fireAddCallbacks(); + + foreach ($this->assets as $asset) { + $callback($asset, $sources); + } + }); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return FilesystemAdapter + */ + public function getAssetsDir(): FilesystemAdapter + { + return $this->assetsDir; + } + + /** + * @param FilesystemAdapter $assetsDir + */ + public function setAssetsDir(FilesystemAdapter $assetsDir) + { + $this->assetsDir = $assetsDir; + } + + /** + * @return string + */ + public function getCacheDir(): ?string + { + return $this->cacheDir; + } + + /** + * @param string $cacheDir + */ + public function setCacheDir(?string $cacheDir) + { + $this->cacheDir = $cacheDir; + } + + /** + * @return array + */ + public function getLessImportDirs(): array + { + return $this->lessImportDirs; + } + + /** + * @param array $lessImportDirs + */ + public function setLessImportDirs(array $lessImportDirs) + { + $this->lessImportDirs = $lessImportDirs; + } +} diff --git a/framework/core/src/Frontend/Content/ContentInterface.php b/framework/core/src/Frontend/Content/ContentInterface.php new file mode 100644 index 000000000..cbf167272 --- /dev/null +++ b/framework/core/src/Frontend/Content/ContentInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Content; + +use Flarum\Frontend\HtmlDocument; +use Psr\Http\Message\ServerRequestInterface as Request; + +interface ContentInterface +{ + /** + * @param HtmlDocument $document + * @param Request $request + */ + public function populate(HtmlDocument $document, Request $request); +} diff --git a/framework/core/src/Frontend/Content/CorePayload.php b/framework/core/src/Frontend/Content/CorePayload.php new file mode 100644 index 000000000..87a390f6d --- /dev/null +++ b/framework/core/src/Frontend/Content/CorePayload.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Content; + +use Flarum\Api\Client; +use Flarum\Api\Controller\ShowUserController; +use Flarum\Frontend\HtmlDocument; +use Flarum\Locale\LocaleManager; +use Flarum\User\User; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface as Request; + +class CorePayload implements ContentInterface +{ + /** + * @var LocaleManager + */ + private $locales; + + /** + * @var Client + */ + private $api; + + /** + * @param LocaleManager $locales + * @param Client $api + */ + public function __construct(LocaleManager $locales, Client $api) + { + $this->locales = $locales; + $this->api = $api; + } + + public function populate(HtmlDocument $document, Request $request) + { + $document->payload = array_merge( + $document->payload, + $this->buildPayload($document, $request) + ); + } + + private function buildPayload(HtmlDocument $document, Request $request) + { + $data = $this->getDataFromApiDocument($document->getForumApiDocument()); + + $actor = $request->getAttribute('actor'); + + if ($actor->exists) { + $user = $this->getUserApiDocument($actor); + $data = array_merge($data, $this->getDataFromApiDocument($user)); + } + + return [ + 'resources' => $data, + 'session' => [ + 'userId' => $actor->id, + 'csrfToken' => $request->getAttribute('session')->token() + ], + 'locales' => $this->locales->getLocales(), + 'locale' => $request->getAttribute('locale') + ]; + } + + private function getDataFromApiDocument(array $apiDocument): array + { + $data[] = $apiDocument['data']; + + if (isset($apiDocument['included'])) { + $data = array_merge($data, $apiDocument['included']); + } + + return $data; + } + + private function getUserApiDocument(User $user): array + { + // TODO: to avoid an extra query, something like + // $controller = new ShowUserController(new PreloadedUserRepository($user)); + + return $this->getResponseBody( + $this->api->send(ShowUserController::class, $user, ['id' => $user->id]) + ); + } + + private function getResponseBody(ResponseInterface $response) + { + return json_decode($response->getBody(), true); + } +} diff --git a/framework/core/src/Frontend/Content/Layout.php b/framework/core/src/Frontend/Content/Layout.php new file mode 100644 index 000000000..07896215d --- /dev/null +++ b/framework/core/src/Frontend/Content/Layout.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Content; + +use Flarum\Frontend\HtmlDocument; +use Psr\Http\Message\ServerRequestInterface as Request; + +class Layout implements ContentInterface +{ + /** + * @var string + */ + protected $layoutView; + + /** + * @param string $layoutView + */ + public function __construct(string $layoutView) + { + $this->layoutView = $layoutView; + } + + public function populate(HtmlDocument $document, Request $request) + { + $document->layoutView = $this->layoutView; + } +} diff --git a/framework/core/src/Frontend/Content/Meta.php b/framework/core/src/Frontend/Content/Meta.php new file mode 100644 index 000000000..9d63c451c --- /dev/null +++ b/framework/core/src/Frontend/Content/Meta.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend\Content; + +use Flarum\Frontend\HtmlDocument; +use Psr\Http\Message\ServerRequestInterface as Request; + +class Meta implements ContentInterface +{ + public function populate(HtmlDocument $document, Request $request) + { + $document->meta = array_merge($document->meta, $this->buildMeta($document)); + $document->head = array_merge($document->head, $this->buildHead($document)); + } + + private function buildMeta(HtmlDocument $document) + { + $forumApiDocument = $document->getForumApiDocument(); + + $meta = [ + 'viewport' => 'width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1', + 'description' => array_get($forumApiDocument, 'data.attributes.forumDescription'), + 'theme-color' => array_get($forumApiDocument, 'data.attributes.themePrimaryColor') + ]; + + return $meta; + } + + private function buildHead(HtmlDocument $document) + { + $head = [ + 'font' => '' + ]; + + if ($faviconUrl = array_get($document->getForumApiDocument(), 'data.attributes.faviconUrl')) { + $head['favicon'] = ''; + } + + return $head; + } +} diff --git a/framework/core/src/Frontend/Controller.php b/framework/core/src/Frontend/Controller.php new file mode 100644 index 000000000..73ba0a08b --- /dev/null +++ b/framework/core/src/Frontend/Controller.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Frontend; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Diactoros\Response\HtmlResponse; + +class Controller implements RequestHandlerInterface +{ + /** + * @var HtmlDocumentFactory + */ + protected $document; + + /** + * @param HtmlDocumentFactory $document + */ + public function __construct(HtmlDocumentFactory $document) + { + $this->document = $document; + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request): Response + { + return new HtmlResponse( + $this->document->make($request)->render() + ); + } +} diff --git a/framework/core/src/Frontend/Event/Rendering.php b/framework/core/src/Frontend/Event/Rendering.php deleted file mode 100644 index a5d81c949..000000000 --- a/framework/core/src/Frontend/Event/Rendering.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend\Event; - -use Flarum\Admin\Controller\FrontendController as AdminFrontendController; -use Flarum\Forum\Controller\FrontendController as ForumFrontendController; -use Flarum\Frontend\AbstractFrontendController; -use Flarum\Frontend\FrontendView; -use Psr\Http\Message\ServerRequestInterface; - -class Rendering -{ - /** - * @var AbstractFrontendController - */ - public $controller; - - /** - * @var FrontendView - */ - public $view; - - /** - * @var ServerRequestInterface - */ - public $request; - - /** - * @param AbstractFrontendController $controller - * @param FrontendView $view - * @param ServerRequestInterface $request - */ - public function __construct(AbstractFrontendController $controller, FrontendView $view, ServerRequestInterface $request) - { - $this->controller = $controller; - $this->view = $view; - $this->request = $request; - } - - public function isForum() - { - return $this->controller instanceof ForumFrontendController; - } - - public function isAdmin() - { - return $this->controller instanceof AdminFrontendController; - } - - public function addAssets($files) - { - foreach ((array) $files as $file) { - $ext = pathinfo($file, PATHINFO_EXTENSION); - - switch ($ext) { - case 'js': - $this->view->getJs()->addFile($file); - break; - - case 'css': - case 'less': - $this->view->getCss()->addFile($file); - break; - } - } - } - - public function addBootstrapper($bootstrapper) - { - $this->view->loadModule($bootstrapper); - } -} diff --git a/framework/core/src/Frontend/FrontendAssets.php b/framework/core/src/Frontend/FrontendAssets.php deleted file mode 100644 index 277b39547..000000000 --- a/framework/core/src/Frontend/FrontendAssets.php +++ /dev/null @@ -1,165 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend; - -use Flarum\Foundation\Application; -use Flarum\Frontend\Asset\JsCompiler; -use Flarum\Frontend\Asset\LessCompiler; -use Flarum\Frontend\Asset\LocaleJsCompiler as LocaleJsCompiler; -use Flarum\Locale\LocaleManager; -use Illuminate\Contracts\Cache\Repository; - -class FrontendAssets -{ - /** - * @var string - */ - protected $name; - - /** - * @var Application - */ - protected $app; - - /** - * @var Repository - */ - protected $cache; - - /** - * @var LocaleManager - */ - protected $locales; - - /** - * @param string $name - * @param Application $app - * @param Repository $cache - * @param LocaleManager $locales - */ - public function __construct($name, Application $app, Repository $cache, LocaleManager $locales) - { - $this->name = $name; - $this->app = $app; - $this->cache = $cache; - $this->locales = $locales; - } - - public function flush() - { - $this->flushJs(); - $this->flushCss(); - } - - public function flushJs() - { - $this->getJs()->flush(); - $this->flushLocaleJs(); - } - - public function flushLocaleJs() - { - foreach ($this->locales->getLocales() as $locale => $info) { - $this->getLocaleJs($locale)->flush(); - } - } - - public function flushCss() - { - $this->getCss()->flush(); - $this->flushLocaleCss(); - } - - public function flushLocaleCss() - { - foreach ($this->locales->getLocales() as $locale => $info) { - $this->getLocaleCss($locale)->flush(); - } - } - - /** - * @return JsCompiler - */ - public function getJs() - { - return new JsCompiler( - $this->getDestination(), - "$this->name.js", - $this->shouldWatch(), - $this->cache - ); - } - - /** - * @return LessCompiler - */ - public function getCss() - { - return new LessCompiler( - $this->getDestination(), - "$this->name.css", - $this->shouldWatch(), - $this->getLessStorage() - ); - } - - /** - * @param $locale - * @return LocaleJsCompiler - */ - public function getLocaleJs($locale) - { - return new LocaleJsCompiler( - $this->getDestination(), - "$this->name-$locale.js", - $this->shouldWatch(), - $this->cache - ); - } - - /** - * @param $locale - * @return LessCompiler - */ - public function getLocaleCss($locale) - { - return new LessCompiler( - $this->getDestination(), - "$this->name-$locale.css", - $this->shouldWatch(), - $this->getLessStorage() - ); - } - - protected function getDestination() - { - return $this->app->publicPath().'/assets'; - } - - protected function shouldWatch() - { - return $this->app->config('debug'); - } - - protected function getLessStorage() - { - return $this->app->storagePath().'/less'; - } - - /** - * @param string $name - */ - public function setName($name) - { - $this->name = $name; - } -} diff --git a/framework/core/src/Frontend/FrontendAssetsFactory.php b/framework/core/src/Frontend/FrontendAssetsFactory.php deleted file mode 100644 index 781963c41..000000000 --- a/framework/core/src/Frontend/FrontendAssetsFactory.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend; - -use Flarum\Foundation\Application; -use Flarum\Locale\LocaleManager; -use Illuminate\Contracts\Cache\Repository; - -class FrontendAssetsFactory -{ - /** - * @var Application - */ - protected $app; - - /** - * @var Repository - */ - protected $cache; - - /** - * @var LocaleManager - */ - protected $locales; - - /** - * @param Application $app - * @param Repository $cache - * @param LocaleManager $locales - */ - public function __construct(Application $app, Repository $cache, LocaleManager $locales) - { - $this->app = $app; - $this->cache = $cache; - $this->locales = $locales; - } - - /** - * @param string $name - * @return FrontendAssets - */ - public function make($name) - { - return new FrontendAssets($name, $this->app, $this->cache, $this->locales); - } -} diff --git a/framework/core/src/Frontend/FrontendServiceProvider.php b/framework/core/src/Frontend/FrontendServiceProvider.php index aa4ba6064..f5ed345d2 100644 --- a/framework/core/src/Frontend/FrontendServiceProvider.php +++ b/framework/core/src/Frontend/FrontendServiceProvider.php @@ -12,14 +12,73 @@ namespace Flarum\Frontend; use Flarum\Foundation\AbstractServiceProvider; +use Flarum\Http\UrlGenerator; +use Illuminate\Contracts\View\Factory as ViewFactory; class FrontendServiceProvider extends AbstractServiceProvider { + public function register() + { + // Yo dawg, I heard you like factories, so I made you a factory to + // create your factory. We expose a couple of factory functions that + // will create frontend factories and configure them with some default + // settings common to both the forum and admin frontends. + + $this->app->singleton('flarum.frontend.assets.defaults', function () { + return function (string $name) { + $assets = new CompilerFactory( + $name, + $this->app->make('filesystem')->disk('flarum-assets'), + $this->app->storagePath() + ); + + $assets->setLessImportDirs([ + $this->app->basePath().'/vendor/components/font-awesome/less' => '' + ]); + + $assets->add(function () use ($name) { + $translations = $this->app->make(Asset\Translations::class); + $translations->setFilter(function (string $id) use ($name) { + return preg_match('/^.+(?:\.|::)(?:'.$name.'|lib)\./', $id); + }); + + return [ + new Asset\CoreAssets($name), + $this->app->make(Asset\LessVariables::class), + $translations, + $this->app->make(Asset\LocaleAssets::class) + ]; + }); + + return $assets; + }; + }); + + $this->app->singleton('flarum.frontend.view.defaults', function () { + return function (string $name) { + $view = $this->app->make(HtmlDocumentFactory::class); + + $view->setCommitAssets($this->app->inDebugMode()); + + $view->add(new Content\Layout('flarum::frontend.'.$name)); + $view->add($this->app->make(Content\CorePayload::class)); + $view->add($this->app->make(Content\Meta::class)); + + return $view; + }; + }); + } + /** * {@inheritdoc} */ public function boot() { $this->loadViewsFrom(__DIR__.'/../../views', 'flarum'); + + $this->app->make(ViewFactory::class)->share([ + 'translator' => $this->app->make('translator'), + 'url' => $this->app->make(UrlGenerator::class) + ]); } } diff --git a/framework/core/src/Frontend/FrontendView.php b/framework/core/src/Frontend/FrontendView.php deleted file mode 100644 index 0063384d3..000000000 --- a/framework/core/src/Frontend/FrontendView.php +++ /dev/null @@ -1,488 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Frontend; - -use Flarum\Api\Client; -use Flarum\Api\Serializer\AbstractSerializer; -use Flarum\Foundation\Application; -use Flarum\Frontend\Asset\CompilerInterface; -use Flarum\Frontend\Asset\LocaleJsCompiler; -use Flarum\Locale\LocaleManager; -use Illuminate\View\Factory; -use Psr\Http\Message\ServerRequestInterface as Request; -use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Resource; - -/** - * This class represents a view which boots up Flarum's client. - */ -class FrontendView -{ - /** - * The title of the document, displayed in the tag. - * - * @var null|string - */ - public $title; - - /** - * The description of the document, displayed in a <meta> tag. - * - * @var null|string - */ - public $description; - - /** - * The language of the document, displayed as the value of the attribute `dir` in the <html> tag. - * - * @var null|string - */ - public $language; - - /** - * The text direction of the document, displayed as the value of the attribute `dir` in the <html> tag. - * - * @var null|string - */ - public $direction; - - /** - * The path to the client layout view to display. - * - * @var string - */ - public $layout; - - /** - * The SEO content of the page, displayed within the layout in <noscript> - * tags. - * - * @var string - */ - public $content; - - /** - * An API response to be preloaded into the page. - * - * This should be a JSON-API document. - * - * @var null|array|object - */ - public $document; - - /** - * Other variables to preload into the page. - * - * @var array - */ - protected $variables = []; - - /** - * An array of JS modules to load before booting the app. - * - * @var array - */ - protected $modules = ['locale']; - - /** - * An array of strings to append to the page's <head>. - * - * @var array - */ - protected $head = []; - - /** - * An array of strings to prepend before the page's </body>. - * - * @var array - */ - protected $foot = []; - - /** - * A map of <link> tags to be generated. - * - * @var array - */ - protected $links = []; - - /** - * @var CompilerInterface - */ - protected $js; - - /** - * @var CompilerInterface - */ - protected $css; - - /** - * @var CompilerInterface - */ - protected $localeJs; - - /** - * @var CompilerInterface - */ - protected $localeCss; - - /** - * @var FrontendAssets - */ - protected $assets; - - /** - * @var Client - */ - protected $api; - - /** - * @var Factory - */ - protected $view; - - /** - * @var LocaleManager - */ - protected $locales; - - /** - * @var AbstractSerializer - */ - protected $userSerializer; - - /** - * @var Application - */ - protected $app; - - /** - * @param string $layout - * @param FrontendAssets $assets - * @param Client $api - * @param Factory $view - * @param LocaleManager $locales - * @param AbstractSerializer $userSerializer - * @param Application $app - */ - public function __construct($layout, FrontendAssets $assets, Client $api, Factory $view, LocaleManager $locales, AbstractSerializer $userSerializer, Application $app) - { - $this->layout = $layout; - $this->api = $api; - $this->assets = $assets; - $this->view = $view; - $this->locales = $locales; - $this->userSerializer = $userSerializer; - $this->app = $app; - - $this->addHeadString('<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600">', 'font'); - - $this->js = $this->assets->getJs(); - $this->css = $this->assets->getCss(); - - $locale = $this->locales->getLocale(); - $this->localeJs = $this->assets->getLocaleJs($locale); - $this->localeCss = $this->assets->getLocaleCss($locale); - - foreach ($this->locales->getJsFiles($locale) as $file) { - $this->localeJs->addFile($file); - } - - foreach ($this->locales->getCssFiles($locale) as $file) { - $this->localeCss->addFile($file); - } - } - - /** - * Add a string to be appended to the page's <head>. - * - * @param string $string - * @param null|string $name - */ - public function addHeadString($string, $name = null) - { - if ($name) { - $this->head[$name] = $string; - } else { - $this->head[] = $string; - } - } - - /** - * Add a string to be prepended before the page's </body>. - * - * @param string $string - */ - public function addFootString($string) - { - $this->foot[] = $string; - } - - /** - * Configure a <link> tag. - * - * @param string $relation - * @param string $target - */ - public function link($relation, $target) - { - $this->links[$relation] = $target; - } - - /** - * Configure the canonical URL for this page. - * - * This will signal to search engines what URL should be used for this - * content, if it can be found under multiple addresses. This is an - * important tool to tackle duplicate content. - * - * @param string $url - */ - public function setCanonicalUrl($url) - { - $this->link('canonical', $url); - } - - /** - * Set a variable to be preloaded into the app. - * - * @param string $name - * @param mixed $value - */ - public function setVariable($name, $value) - { - $this->variables[$name] = $value; - } - - /** - * Add a JavaScript module to be imported before the app is booted. - * - * @param string $module - */ - public function loadModule($module) - { - $this->modules[] = $module; - } - - /** - * Get the string contents of the view. - * - * @param Request $request - * @return string - */ - public function render(Request $request) - { - $forum = $this->getForumDocument($request); - - $this->view->share('translator', $this->locales->getTranslator()); - $this->view->share('allowJs', ! array_get($request->getQueryParams(), 'nojs')); - $this->view->share('forum', array_get($forum, 'data')); - $this->view->share('debug', $this->app->inDebugMode()); - - $view = $this->view->make('flarum.forum::frontend.app'); - - $view->title = $this->buildTitle(array_get($forum, 'data.attributes.title')); - $view->description = $this->description ?: array_get($forum, 'data.attributes.description'); - $view->language = $this->language ?: $this->locales->getLocale(); - $view->direction = $this->direction ?: 'ltr'; - - $view->modules = $this->modules; - $view->payload = $this->buildPayload($request, $forum); - - $view->layout = $this->buildLayout(); - - $baseUrl = array_get($forum, 'data.attributes.baseUrl'); - $view->cssUrls = $this->buildCssUrls($baseUrl); - $view->jsUrls = $this->buildJsUrls($baseUrl); - - $view->head = $this->buildHeadContent(); - $view->foot = $this->buildFootContent(array_get($forum, 'data.attributes.footerHtml')); - - return $view->render(); - } - - protected function buildTitle($forumTitle) - { - return ($this->title ? $this->title.' - ' : '').$forumTitle; - } - - protected function buildPayload(Request $request, $forum) - { - $data = $this->getDataFromDocument($forum); - - if ($request->getAttribute('actor')->exists) { - $user = $this->getUserDocument($request); - $data = array_merge($data, $this->getDataFromDocument($user)); - } - - $payload = [ - 'resources' => $data, - 'session' => $this->buildSession($request), - 'document' => $this->document, - 'locales' => $this->locales->getLocales(), - 'locale' => $this->locales->getLocale() - ]; - - return array_merge($payload, $this->variables); - } - - protected function buildLayout() - { - $view = $this->view->make($this->layout); - - $view->content = $this->buildContent(); - - return $view; - } - - protected function buildContent() - { - $view = $this->view->make('flarum.forum::frontend.content'); - - $view->content = $this->content; - - return $view; - } - - protected function buildCssUrls($baseUrl) - { - return $this->buildAssetUrls($baseUrl, [$this->css->getFile(), $this->localeCss->getFile()]); - } - - protected function buildJsUrls($baseUrl) - { - return $this->buildAssetUrls($baseUrl, [$this->js->getFile(), $this->localeJs->getFile()]); - } - - protected function buildAssetUrls($baseUrl, $files) - { - return array_map(function ($file) use ($baseUrl) { - return $baseUrl.str_replace(public_path(), '', $file); - }, array_filter($files)); - } - - protected function buildHeadContent() - { - $html = implode("\n", $this->head); - - foreach ($this->links as $rel => $href) { - $html .= "\n<link rel=\"$rel\" href=\"$href\" />"; - } - - return $html; - } - - protected function buildFootContent($customFooterHtml) - { - return implode("\n", $this->foot)."\n".$customFooterHtml; - } - - /** - * @return CompilerInterface - */ - public function getJs() - { - return $this->js; - } - - /** - * @return CompilerInterface - */ - public function getCss() - { - return $this->css; - } - - /** - * @return LocaleJsCompiler - */ - public function getLocaleJs() - { - return $this->localeJs; - } - - /** - * @return CompilerInterface - */ - public function getLocaleCss() - { - return $this->localeCss; - } - - /** - * Get the result of an API request to show the forum. - * - * @param Request $request - * @return array - */ - protected function getForumDocument(Request $request) - { - $actor = $request->getAttribute('actor'); - - $response = $this->api->send('Flarum\Api\Controller\ShowForumController', $actor); - - return json_decode($response->getBody(), true); - } - - /** - * Get the result of an API request to show the current user. - * - * @param Request $request - * @return array - */ - protected function getUserDocument(Request $request) - { - $actor = $request->getAttribute('actor'); - - $this->userSerializer->setActor($actor); - - $resource = new Resource($actor, $this->userSerializer); - - $document = new Document($resource->with('groups')); - - return $document->toArray(); - } - - /** - * Get information about the current session. - * - * @param Request $request - * @return array - */ - protected function buildSession(Request $request) - { - $actor = $request->getAttribute('actor'); - $session = $request->getAttribute('session'); - - return [ - 'userId' => $actor->id, - 'csrfToken' => $session->token() - ]; - } - - /** - * Get an array of data by merging the 'data' and 'included' keys of a - * JSON-API document. - * - * @param array $document - * @return array - */ - private function getDataFromDocument(array $document) - { - $data[] = $document['data']; - - if (isset($document['included'])) { - $data = array_merge($data, $document['included']); - } - - return $data; - } -} diff --git a/framework/core/src/Frontend/FrontendViewFactory.php b/framework/core/src/Frontend/FrontendViewFactory.php deleted file mode 100644 index 82682c22a..000000000 --- a/framework/core/src/Frontend/FrontendViewFactory.php +++ /dev/null @@ -1,72 +0,0 @@ -<?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\Frontend; - -use Flarum\Api\Client; -use Flarum\Api\Serializer\CurrentUserSerializer; -use Flarum\Foundation\Application; -use Flarum\Locale\LocaleManager; -use Illuminate\Contracts\View\Factory; - -class FrontendViewFactory -{ - /** - * @var Client - */ - protected $api; - - /** - * @var Factory - */ - protected $view; - - /** - * @var LocaleManager - */ - protected $locales; - - /** - * @var CurrentUserSerializer - */ - protected $userSerializer; - - /** - * @var Application - */ - protected $app; - - /** - * @param Client $api - * @param Factory $view - * @param LocaleManager $locales - * @param CurrentUserSerializer $userSerializer - * @param Application $app - */ - public function __construct(Client $api, Factory $view, LocaleManager $locales, CurrentUserSerializer $userSerializer, Application $app) - { - $this->api = $api; - $this->view = $view; - $this->locales = $locales; - $this->userSerializer = $userSerializer; - $this->app = $app; - } - - /** - * @param string $layout - * @param FrontendAssets $assets - * @return FrontendView - */ - public function make($layout, FrontendAssets $assets) - { - return new FrontendView($layout, $assets, $this->api, $this->view, $this->locales, $this->userSerializer, $this->app); - } -} diff --git a/framework/core/src/Frontend/HtmlDocument.php b/framework/core/src/Frontend/HtmlDocument.php new file mode 100644 index 000000000..f21beb2c5 --- /dev/null +++ b/framework/core/src/Frontend/HtmlDocument.php @@ -0,0 +1,251 @@ +<?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\Frontend; + +use Illuminate\Contracts\Support\Renderable; +use Illuminate\Contracts\View\Factory; +use Illuminate\Contracts\View\View; + +/** + * A view which renders a HTML skeleton for Flarum's frontend app. + */ +class HtmlDocument implements Renderable +{ + /** + * The title of the document, displayed in the <title> tag. + * + * @var null|string + */ + public $title; + + /** + * The language of the document, displayed as the value of the attribute `lang` in the <html> tag. + * + * @var null|string + */ + public $language; + + /** + * The text direction of the document, displayed as the value of the attribute `dir` in the <html> tag. + * + * @var null|string + */ + public $direction; + + /** + * The name of the frontend app view to display. + * + * @var string + */ + public $appView = 'flarum::frontend.app'; + + /** + * The name of the frontend layout view to display. + * + * @var string + */ + public $layoutView; + + /** + * The name of the frontend content view to display. + * + * @var string + */ + public $contentView = 'flarum::frontend.content'; + + /** + * The SEO content of the page, displayed within the layout in <noscript> tags. + * + * @var string|Renderable + */ + public $content; + + /** + * Other variables to preload into the Flarum JS. + * + * @var array + */ + public $payload = []; + + /** + * An array of meta tags to append to the page's <head>. + * + * @var array + */ + public $meta = []; + + /** + * The canonical URL for this page. + * + * This will signal to search engines what URL should be used for this + * content, if it can be found under multiple addresses. This is an + * important tool to tackle duplicate content. + * + * @var null|string + */ + public $canonicalUrl; + + /** + * An array of strings to append to the page's <head>. + * + * @var array + */ + public $head = []; + + /** + * An array of strings to prepend before the page's </body>. + * + * @var array + */ + public $foot = []; + + /** + * An array of JavaScript URLs to load. + * + * @var array + */ + public $js = []; + + /** + * An array of CSS URLs to load. + * + * @var array + */ + public $css = []; + + /** + * @var Factory + */ + protected $view; + + /** + * @var array + */ + protected $forumApiDocument; + + /** + * @param Factory $view + * @param array $forumApiDocument + */ + public function __construct(Factory $view, array $forumApiDocument) + { + $this->view = $view; + $this->forumApiDocument = $forumApiDocument; + } + + /** + * @return string + */ + public function render(): string + { + $this->view->share('forum', array_get($this->forumApiDocument, 'data.attributes')); + + return $this->makeView()->render(); + } + + /** + * @return View + */ + protected function makeView(): View + { + return $this->view->make($this->appView)->with([ + 'title' => $this->makeTitle(), + 'payload' => $this->payload, + 'layout' => $this->makeLayout(), + 'language' => $this->language, + 'direction' => $this->direction, + 'js' => $this->makeJs(), + 'head' => $this->makeHead(), + 'foot' => $this->makeFoot(), + ]); + } + + /** + * @return string + */ + protected function makeTitle(): string + { + return ($this->title ? $this->title.' - ' : '').array_get($this->forumApiDocument, 'data.attributes.title'); + } + + /** + * @return View + */ + protected function makeLayout(): View + { + if ($this->layoutView) { + return $this->view->make($this->layoutView)->with('content', $this->makeContent()); + } + } + + /** + * @return View + */ + protected function makeContent(): View + { + return $this->view->make($this->contentView)->with('content', $this->content); + } + + /** + * @return string + */ + protected function makeHead(): string + { + $head = array_map(function ($url) { + return '<link rel="stylesheet" href="'.e($url).'">'; + }, $this->css); + + if ($this->canonicalUrl) { + $head[] = '<link rel="canonical" href="'.e($this->canonicalUrl).'">'; + } + + $head = array_merge($head, array_map(function ($content, $name) { + return '<meta name="'.e($name).'" content="'.e($content).'">'; + }, $this->meta, array_keys($this->meta))); + + return implode("\n", array_merge($head, $this->head)); + } + + /** + * @return string + */ + protected function makeJs(): string + { + return implode("\n", array_map(function ($url) { + return '<script src="'.e($url).'"></script>'; + }, $this->js)); + } + + /** + * @return string + */ + protected function makeFoot(): string + { + return implode("\n", $this->foot); + } + + /** + * @return array + */ + public function getForumApiDocument(): array + { + return $this->forumApiDocument; + } + + /** + * @param array $forumApiDocument + */ + public function setForumApiDocument(array $forumApiDocument) + { + $this->forumApiDocument = $forumApiDocument; + } +} diff --git a/framework/core/src/Frontend/HtmlDocumentFactory.php b/framework/core/src/Frontend/HtmlDocumentFactory.php new file mode 100644 index 000000000..aa8871327 --- /dev/null +++ b/framework/core/src/Frontend/HtmlDocumentFactory.php @@ -0,0 +1,180 @@ +<?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\Frontend; + +use Flarum\Api\Client; +use Flarum\Api\Controller\ShowForumController; +use Flarum\Frontend\Compiler\CompilerInterface; +use Flarum\Frontend\Content\ContentInterface; +use Illuminate\Contracts\View\Factory; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class HtmlDocumentFactory +{ + /** + * @var Factory + */ + protected $view; + + /** + * @var Client + */ + protected $api; + + /** + * @var CompilerFactory + */ + protected $assets; + + /** + * @var bool + */ + protected $commitAssets; + + /** + * @var ContentInterface[] + */ + protected $content = []; + + /** + * @param Factory $view + * @param Client $api + * @param CompilerFactory|null $assets + * @param bool $commitAssets + */ + public function __construct(Factory $view, Client $api, CompilerFactory $assets = null, bool $commitAssets = false) + { + $this->view = $view; + $this->api = $api; + $this->assets = $assets; + $this->commitAssets = $commitAssets; + } + + /** + * @param ContentInterface $content + */ + public function add($content) + { + $this->content[] = $content; + } + + /** + * @param Request $request + * @return HtmlDocument + */ + public function make(Request $request): HtmlDocument + { + $forumDocument = $this->getForumDocument($request); + + $view = new HtmlDocument($this->view, $forumDocument); + + $locale = $request->getAttribute('locale'); + + $js = [$this->assets->makeJs(), $this->assets->makeLocaleJs($locale)]; + $css = [$this->assets->makeCss(), $this->assets->makeLocaleCss($locale)]; + + $this->maybeCommitAssets(array_merge($js, $css)); + + $view->js = array_merge($view->js, $this->getUrls($js)); + $view->css = array_merge($view->css, $this->getUrls($css)); + + $this->populate($view, $request); + + return $view; + } + + /** + * @return CompilerFactory + */ + public function getAssets(): CompilerFactory + { + return $this->assets; + } + + /** + * @param CompilerFactory $assets + */ + public function setAssets(CompilerFactory $assets) + { + $this->assets = $assets; + } + + /** + * @param HtmlDocument $view + * @param Request $request + */ + protected function populate(HtmlDocument $view, Request $request) + { + foreach ($this->content as $content) { + $content->populate($view, $request); + } + } + + /** + * @param Request $request + * @return array + */ + private function getForumDocument(Request $request): array + { + $actor = $request->getAttribute('actor'); + + return $this->getResponseBody( + $this->api->send(ShowForumController::class, $actor) + ); + } + + /** + * @param Response $response + * @return array + */ + private function getResponseBody(Response $response) + { + return json_decode($response->getBody(), true); + } + + private function maybeCommitAssets(array $compilers) + { + if ($this->commitAssets) { + foreach ($compilers as $compiler) { + $compiler->commit(); + } + } + } + + /** + * @param CompilerInterface[] $compilers + * @return string[] + */ + private function getUrls(array $compilers) + { + return array_filter(array_map(function (CompilerInterface $compiler) { + return $compiler->getUrl(); + }, $compilers)); + } + + /** + * @return bool + */ + public function getCommitAssets(): bool + { + return $this->commitAssets; + } + + /** + * @param bool $commitAssets + */ + public function setCommitAssets(bool $commitAssets) + { + $this->commitAssets = $commitAssets; + } +} diff --git a/framework/core/src/Frontend/RecompileFrontendAssets.php b/framework/core/src/Frontend/RecompileFrontendAssets.php new file mode 100644 index 000000000..9dd31f5d6 --- /dev/null +++ b/framework/core/src/Frontend/RecompileFrontendAssets.php @@ -0,0 +1,84 @@ +<?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\Frontend; + +use Flarum\Extension\Event\Disabled; +use Flarum\Extension\Event\Enabled; +use Flarum\Foundation\Event\ClearingCache; +use Flarum\Locale\LocaleManager; +use Flarum\Settings\Event\Saved; +use Illuminate\Contracts\Events\Dispatcher; + +class RecompileFrontendAssets +{ + /** + * @var CompilerFactory + */ + protected $assets; + + /** + * @var LocaleManager + */ + protected $locales; + + /** + * @param CompilerFactory $assets + * @param LocaleManager $locales + */ + public function __construct(CompilerFactory $assets, LocaleManager $locales) + { + $this->assets = $assets; + $this->locales = $locales; + } + + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) + { + $events->listen(Saved::class, [$this, 'whenSettingsSaved']); + $events->listen(Enabled::class, [$this, 'flush']); + $events->listen(Disabled::class, [$this, 'flush']); + $events->listen(ClearingCache::class, [$this, 'flush']); + } + + public function whenSettingsSaved(Saved $event) + { + if (preg_grep('/^theme_/i', array_keys($event->settings))) { + $this->flushCss(); + } + } + + public function flush() + { + $this->flushCss(); + $this->flushJs(); + } + + protected function flushCss() + { + $this->assets->makeCss()->flush(); + + foreach ($this->locales->getLocales() as $locale => $name) { + $this->assets->makeLocaleCss($locale)->flush(); + } + } + + protected function flushJs() + { + $this->assets->makeJs()->flush(); + + foreach ($this->locales->getLocales() as $locale => $name) { + $this->assets->makeLocaleJs($locale)->flush(); + } + } +} diff --git a/framework/core/src/Http/ControllerRouteHandler.php b/framework/core/src/Http/ControllerRouteHandler.php index a37b776ed..58c7d0550 100644 --- a/framework/core/src/Http/ControllerRouteHandler.php +++ b/framework/core/src/Http/ControllerRouteHandler.php @@ -25,13 +25,13 @@ class ControllerRouteHandler protected $container; /** - * @var string + * @var string|callable */ protected $controller; /** * @param Container $container - * @param string $controller + * @param string|callable $controller */ public function __construct(Container $container, $controller) { @@ -54,12 +54,16 @@ class ControllerRouteHandler } /** - * @param string $class + * @param string|callable $class * @return RequestHandlerInterface */ protected function resolveController($class) { - $controller = $this->container->make($class); + if (is_callable($class)) { + $controller = $this->container->call($class); + } else { + $controller = $this->container->make($class); + } if (! ($controller instanceof RequestHandlerInterface)) { throw new InvalidArgumentException( diff --git a/framework/core/src/Http/Middleware/SetLocale.php b/framework/core/src/Http/Middleware/SetLocale.php index bcb37a9da..6e07797a8 100644 --- a/framework/core/src/Http/Middleware/SetLocale.php +++ b/framework/core/src/Http/Middleware/SetLocale.php @@ -46,6 +46,8 @@ class SetLocale implements Middleware $this->locales->setLocale($locale); } + $request = $request->withAttribute('locale', $this->locales->getLocale()); + return $handler->handle($request); } } diff --git a/framework/core/src/Http/RouteHandlerFactory.php b/framework/core/src/Http/RouteHandlerFactory.php index 4831bb39c..1a346c0da 100644 --- a/framework/core/src/Http/RouteHandlerFactory.php +++ b/framework/core/src/Http/RouteHandlerFactory.php @@ -11,6 +11,7 @@ namespace Flarum\Http; +use Flarum\Frontend\Controller as FrontendController; use Illuminate\Contracts\Container\Container; class RouteHandlerFactory @@ -29,11 +30,47 @@ class RouteHandlerFactory } /** - * @param string $controller + * @param string|callable $controller * @return ControllerRouteHandler */ public function toController($controller) { return new ControllerRouteHandler($this->container, $controller); } + + /** + * @param string $frontend + * @param string|null $content + * @return ControllerRouteHandler + */ + public function toFrontend(string $frontend, string $content = null) + { + return $this->toController(function (Container $container) use ($frontend, $content) { + $frontend = $container->make($frontend); + + if ($content) { + $frontend->add($container->make($content)); + } + + return new FrontendController($frontend); + }); + } + + /** + * @param string|null $content + * @return ControllerRouteHandler + */ + public function toForum(string $content = null) + { + return $this->toFrontend('flarum.forum.frontend', $content); + } + + /** + * @param string|null $content + * @return ControllerRouteHandler + */ + public function toAdmin(string $content = null) + { + return $this->toFrontend('flarum.admin.frontend', $content); + } } diff --git a/framework/core/src/Locale/LocaleServiceProvider.php b/framework/core/src/Locale/LocaleServiceProvider.php index 69ce0d98b..74ab5331d 100644 --- a/framework/core/src/Locale/LocaleServiceProvider.php +++ b/framework/core/src/Locale/LocaleServiceProvider.php @@ -25,7 +25,7 @@ class LocaleServiceProvider extends AbstractServiceProvider */ public function boot(Dispatcher $events) { - $locales = $this->app->make('flarum.localeManager'); + $locales = $this->app->make('flarum.locales'); $locales->addLocale($this->getDefaultLocale(), 'Default'); @@ -38,7 +38,7 @@ class LocaleServiceProvider extends AbstractServiceProvider public function register() { $this->app->singleton(LocaleManager::class); - $this->app->alias(LocaleManager::class, 'flarum.localeManager'); + $this->app->alias(LocaleManager::class, 'flarum.locales'); $this->app->singleton('translator', function () { $translator = new Translator($this->getDefaultLocale(), new MessageSelector()); diff --git a/framework/core/src/Settings/Event/Saved.php b/framework/core/src/Settings/Event/Saved.php index 216a68411..03eb042cf 100644 --- a/framework/core/src/Settings/Event/Saved.php +++ b/framework/core/src/Settings/Event/Saved.php @@ -14,26 +14,15 @@ namespace Flarum\Settings\Event; class Saved { /** - * The setting key that was set. - * - * @var string + * @var array */ - public $key; + public $settings; /** - * The setting value that was set. - * - * @var string + * @param array $settings */ - public $value; - - /** - * @param string $key The setting key that was set. - * @param string $value The setting value that was set. - */ - public function __construct($key, $value) + public function __construct(array $settings) { - $this->key = $key; - $this->value = $value; + $this->settings = $settings; } } diff --git a/framework/core/src/Admin/Frontend.php b/framework/core/src/Settings/Event/Saving.php similarity index 53% rename from framework/core/src/Admin/Frontend.php rename to framework/core/src/Settings/Event/Saving.php index b984e15e2..d54d289fd 100644 --- a/framework/core/src/Admin/Frontend.php +++ b/framework/core/src/Settings/Event/Saving.php @@ -9,17 +9,20 @@ * file that was distributed with this source code. */ -namespace Flarum\Admin; +namespace Flarum\Settings\Event; -use Flarum\Frontend\AbstractFrontend; - -class Frontend extends AbstractFrontend +class Saving { /** - * {@inheritdoc} + * @var array */ - protected function getName() + public $settings; + + /** + * @param array $settings + */ + public function __construct(array &$settings) { - return 'admin'; + $this->settings = &$settings; } } diff --git a/framework/core/src/Settings/OverrideSettingsRepository.php b/framework/core/src/Settings/OverrideSettingsRepository.php new file mode 100644 index 000000000..a7d89066b --- /dev/null +++ b/framework/core/src/Settings/OverrideSettingsRepository.php @@ -0,0 +1,61 @@ +<?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\Settings; + +/** + * A settings repository decorator that allows overriding certain values. + * + * The `OverrideSettingsRepository` class decorates another + * `SettingsRepositoryInterface` instance but allows certain settings to be + * overridden with predefined values. It does not affect writing methods. + * + * Within Flarum, this can be used to test out new setting values in a system + * before they are committed to the database. + * + * @see \Flarum\Forum\RecompileFrontendAssets For an example usage. + */ +class OverrideSettingsRepository implements SettingsRepositoryInterface +{ + protected $inner; + + protected $overrides = []; + + public function __construct(SettingsRepositoryInterface $inner, array $overrides) + { + $this->inner = $inner; + $this->overrides = $overrides; + } + + public function all() + { + return array_merge($this->inner->all(), $this->overrides); + } + + public function get($key, $default = null) + { + if (array_key_exists($key, $this->overrides)) { + return $this->overrides[$key]; + } + + return array_get($this->all(), $key, $default); + } + + public function set($key, $value) + { + $this->inner->set($key, $value); + } + + public function delete($key) + { + $this->inner->delete($key); + } +} diff --git a/framework/core/views/frontend/admin.blade.php b/framework/core/views/frontend/admin.blade.php index 47275da32..a5ca2b803 100644 --- a/framework/core/views/frontend/admin.blade.php +++ b/framework/core/views/frontend/admin.blade.php @@ -1,37 +1,37 @@ <div id="app" class="App"> - <div id="app-navigation" class="App-navigation"></div> + <div id="app-navigation" class="App-navigation"></div> - <div id="drawer" class="App-drawer"> + <div id="drawer" class="App-drawer"> - <header id="header" class="App-header"> - <div id="header-navigation" class="Header-navigation"></div> - <div class="container"> - <h1 class="Header-title"> - <a href="{{ array_get($forum, 'attributes.baseUrl') }}"> - <?php $title = array_get($forum, 'attributes.title'); ?> - @if ($logo = array_get($forum, 'attributes.logoUrl')) - <img src="{{ $logo }}" alt="{{ $title }}" class="Header-logo"> - @else - {{ $title }} - @endif - </a> - </h1> - <div id="header-primary" class="Header-primary"></div> - <div id="header-secondary" class="Header-secondary"></div> - </div> - </header> + <header id="header" class="App-header"> + <div id="header-navigation" class="Header-navigation"></div> + <div class="container"> + <h1 class="Header-title"> + <a href="{{ array_get($forum, 'baseUrl') }}"> + <?php $title = array_get($forum, 'title'); ?> + @if ($logo = array_get($forum, 'logoUrl')) + <img src="{{ $logo }}" alt="{{ $title }}" class="Header-logo"> + @else + {{ $title }} + @endif + </a> + </h1> + <div id="header-primary" class="Header-primary"></div> + <div id="header-secondary" class="Header-secondary"></div> + </div> + </header> - </div> - - <main class="App-content"> - <div class="container"> - <div id="admin-navigation" class="App-nav sideNav"></div> </div> - <div id="content" class="sideNavOffset"></div> + <main class="App-content"> + <div class="container"> + <div id="admin-navigation" class="App-nav sideNav"></div> + </div> - {!! $content !!} - </main> + <div id="content" class="sideNavOffset"></div> + + {!! $content !!} + </main> </div> diff --git a/framework/core/views/frontend/app.blade.php b/framework/core/views/frontend/app.blade.php index 4d69331e3..b1f56b215 100644 --- a/framework/core/views/frontend/app.blade.php +++ b/framework/core/views/frontend/app.blade.php @@ -1,63 +1,40 @@ <!doctype html> <html dir="{{ $direction }}" lang="{{ $language }}"> - <head> - <meta charset="utf-8"> - <title>{{ $title }} - - - - @if (! $allowJs) - - @endif + + + {{ $title }} - @foreach ($cssUrls as $url) - - @endforeach + {!! $head !!} + - @if ($faviconUrl = array_get($forum, 'attributes.faviconUrl')) - - @endif + + {!! $layout !!} - {!! $head !!} - + +
- - {!! $layout !!} + - -
+ {!! $js !!} - @if ($allowJs) - + - @endforeach + try { + flarum.core.app.load(@json($payload)); + flarum.core.app.bootExtensions(flarum.extensions); + flarum.core.app.boot(); + } catch (e) { + var error = document.getElementById('flarum-loading-error'); + error.innerHTML += document.getElementById('flarum-content').textContent; + error.style.display = 'block'; + throw e; + } + - - @else - - @endif - - {!! $foot !!} - + {!! $foot !!} + diff --git a/framework/core/views/frontend/content.blade.php b/framework/core/views/frontend/content.blade.php index 6dc2bca33..37918e34b 100644 --- a/framework/core/views/frontend/content.blade.php +++ b/framework/core/views/frontend/content.blade.php @@ -1,23 +1,23 @@ -@if ($allowJs) - diff --git a/framework/core/views/frontend/content/discussion.blade.php b/framework/core/views/frontend/content/discussion.blade.php index 58a688ce8..cc597dc50 100644 --- a/framework/core/views/frontend/content/discussion.blade.php +++ b/framework/core/views/frontend/content/discussion.blade.php @@ -1,5 +1,5 @@
-

{{ $document->data->attributes->title }}

+

{{ $apiDocument->data->attributes->title }}

@foreach ($posts as $post) @@ -15,11 +15,11 @@ @endforeach
- @if (isset($document->links->prev)) + @if (isset($apiDocument->links->prev)) « {{ $translator->trans('core.views.discussion.previous_page_button') }} @endif - @if (isset($document->links->next)) + @if (isset($apiDocument->links->next)) {{ $translator->trans('core.views.discussion.next_page_button') }} » @endif
diff --git a/framework/core/views/frontend/content/index.blade.php b/framework/core/views/frontend/content/index.blade.php index d5c4040ab..809a3bd3a 100644 --- a/framework/core/views/frontend/content/index.blade.php +++ b/framework/core/views/frontend/content/index.blade.php @@ -4,7 +4,7 @@

{{ $translator->trans('core.views.index.all_discussions_heading') }}