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