Frontend refactor (#1471)

Refactor Frontend + Asset code

- Use Laravel's Filesystem component for asset IO, meaning theoretically
  assets should be storable on S3 etc.

- More reliable checking for asset recompilation when debug mode is on,
  so you don't have to constantly delete the compiled assets to force
  a recompile. Should also fix issues with locale JS files being
  recompiled with the same name and cached.

- Remove JavaScript minification, because it will be done by Webpack
  (exception is for the TextFormatter JS).

- Add support for JS sourcemaps.

- Separate frontend view and assets completely. This is an important
  distinction because frontend assets are compiled independent of a
  request, whereas putting together a view depends on a request.

- Bind frontend view/asset factory instances to the container (in
  service providers) rather than subclassing. Asset and content
  populators can be added to these factories – these are simply objects
  that populate the asset compilers or the view with information.

- Add RouteHandlerFactory functions that make it easy to hook up a
  frontend controller with a frontend instance ± some content.

- Remove the need for "nojs"

- Fix cache:clear command

- Recompile assets when settings/enabled extensions change
This commit is contained in:
Toby Zerner 2018-06-30 12:31:12 +09:30 committed by GitHub
parent 92d5f8f1b3
commit 651a6bf4ea
73 changed files with 2846 additions and 2176 deletions

View File

@ -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",

View File

@ -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];

View File

@ -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();
}
}

View File

@ -1,43 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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(),
]);
}
}
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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;
}
}

View File

@ -1,79 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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;
}
}

View File

@ -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()
);
};

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -196,6 +196,7 @@ class Formatter
$configurator = $this->getConfigurator();
$configurator->enableJavaScript();
$configurator->javascript->exportMethods = ['preview'];
$configurator->javascript->setMinifier('MatthiasMullieMinify');
return $configurator->finalize([
'returnParser' => false,

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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)
{
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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)
{
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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'));
}
}

View File

@ -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();

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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());
}
}

View File

@ -1,30 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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);
}
}

View File

@ -1,28 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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;
}
}

View File

@ -1,87 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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());
}
}

View File

@ -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();
}
}

View File

@ -1,68 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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';
}
}

View File

@ -0,0 +1,110 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\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();
}
}
}

View File

@ -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(

View File

@ -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);
}
}

View File

@ -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
{
}

View File

@ -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'

View File

@ -1,184 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\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);
}
}

View File

@ -1,57 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\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();
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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)
{
}
}

View File

@ -0,0 +1,80 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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)
{
}
}

View File

@ -1,51 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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;
}
}

View File

@ -1,70 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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?
}
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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)
{
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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)
{
}
}

View File

@ -1,33 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}
}

View File

@ -1,207 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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)
{
}
}

View File

@ -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();
}

View File

@ -0,0 +1,84 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}
}
}

View File

@ -0,0 +1,96 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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();
}
}

View File

@ -0,0 +1,276 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}
}
}

View File

@ -0,0 +1,52 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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;
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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;
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\Compiler\Source;
interface SourceInterface
{
/**
* @return string
*/
public function getContent(): string;
/**
* @return mixed
*/
public function getCacheDifferentiator();
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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();
}
}

View File

@ -0,0 +1,239 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\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;
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}

View File

@ -0,0 +1,99 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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;
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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' => '<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600">'
];
if ($faviconUrl = array_get($document->getForumApiDocument(), 'data.attributes.faviconUrl')) {
$head['favicon'] = '<link rel="shortcut icon" href="'.e($faviconUrl).'">';
}
return $head;
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use 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()
);
}
}

View File

@ -1,81 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend\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);
}
}

View File

@ -1,165 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\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;
}
}

View File

@ -1,55 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\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);
}
}

View File

@ -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)
]);
}
}

View File

@ -1,488 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Api\Client;
use Flarum\Api\Serializer\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 <title> tag.
*
* @var null|string
*/
public $title;
/**
* The description of the document, displayed in a <meta> tag.
*
* @var null|string
*/
public $description;
/**
* The language of the document, displayed as the value of the attribute `dir` in the <html> tag.
*
* @var null|string
*/
public $language;
/**
* The text direction of the document, displayed as the value of the attribute `dir` in the <html> tag.
*
* @var null|string
*/
public $direction;
/**
* The path to the client layout view to display.
*
* @var string
*/
public $layout;
/**
* The SEO content of the page, displayed within the layout in <noscript>
* tags.
*
* @var string
*/
public $content;
/**
* An API response to be preloaded into the page.
*
* This should be a JSON-API document.
*
* @var null|array|object
*/
public $document;
/**
* Other variables to preload into the page.
*
* @var array
*/
protected $variables = [];
/**
* An array of JS modules to load before booting the app.
*
* @var array
*/
protected $modules = ['locale'];
/**
* An array of strings to append to the page's <head>.
*
* @var array
*/
protected $head = [];
/**
* An array of strings to prepend before the page's </body>.
*
* @var array
*/
protected $foot = [];
/**
* A map of <link> tags to be generated.
*
* @var array
*/
protected $links = [];
/**
* @var CompilerInterface
*/
protected $js;
/**
* @var CompilerInterface
*/
protected $css;
/**
* @var CompilerInterface
*/
protected $localeJs;
/**
* @var CompilerInterface
*/
protected $localeCss;
/**
* @var FrontendAssets
*/
protected $assets;
/**
* @var Client
*/
protected $api;
/**
* @var Factory
*/
protected $view;
/**
* @var LocaleManager
*/
protected $locales;
/**
* @var AbstractSerializer
*/
protected $userSerializer;
/**
* @var Application
*/
protected $app;
/**
* @param string $layout
* @param FrontendAssets $assets
* @param Client $api
* @param Factory $view
* @param LocaleManager $locales
* @param AbstractSerializer $userSerializer
* @param Application $app
*/
public function __construct($layout, FrontendAssets $assets, Client $api, Factory $view, LocaleManager $locales, AbstractSerializer $userSerializer, Application $app)
{
$this->layout = $layout;
$this->api = $api;
$this->assets = $assets;
$this->view = $view;
$this->locales = $locales;
$this->userSerializer = $userSerializer;
$this->app = $app;
$this->addHeadString('<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600">', 'font');
$this->js = $this->assets->getJs();
$this->css = $this->assets->getCss();
$locale = $this->locales->getLocale();
$this->localeJs = $this->assets->getLocaleJs($locale);
$this->localeCss = $this->assets->getLocaleCss($locale);
foreach ($this->locales->getJsFiles($locale) as $file) {
$this->localeJs->addFile($file);
}
foreach ($this->locales->getCssFiles($locale) as $file) {
$this->localeCss->addFile($file);
}
}
/**
* Add a string to be appended to the page's <head>.
*
* @param string $string
* @param null|string $name
*/
public function addHeadString($string, $name = null)
{
if ($name) {
$this->head[$name] = $string;
} else {
$this->head[] = $string;
}
}
/**
* Add a string to be prepended before the page's </body>.
*
* @param string $string
*/
public function addFootString($string)
{
$this->foot[] = $string;
}
/**
* Configure a <link> tag.
*
* @param string $relation
* @param string $target
*/
public function link($relation, $target)
{
$this->links[$relation] = $target;
}
/**
* Configure the canonical URL for this page.
*
* This will signal to search engines what URL should be used for this
* content, if it can be found under multiple addresses. This is an
* important tool to tackle duplicate content.
*
* @param string $url
*/
public function setCanonicalUrl($url)
{
$this->link('canonical', $url);
}
/**
* Set a variable to be preloaded into the app.
*
* @param string $name
* @param mixed $value
*/
public function setVariable($name, $value)
{
$this->variables[$name] = $value;
}
/**
* Add a JavaScript module to be imported before the app is booted.
*
* @param string $module
*/
public function loadModule($module)
{
$this->modules[] = $module;
}
/**
* Get the string contents of the view.
*
* @param Request $request
* @return string
*/
public function render(Request $request)
{
$forum = $this->getForumDocument($request);
$this->view->share('translator', $this->locales->getTranslator());
$this->view->share('allowJs', ! array_get($request->getQueryParams(), 'nojs'));
$this->view->share('forum', array_get($forum, 'data'));
$this->view->share('debug', $this->app->inDebugMode());
$view = $this->view->make('flarum.forum::frontend.app');
$view->title = $this->buildTitle(array_get($forum, 'data.attributes.title'));
$view->description = $this->description ?: array_get($forum, 'data.attributes.description');
$view->language = $this->language ?: $this->locales->getLocale();
$view->direction = $this->direction ?: 'ltr';
$view->modules = $this->modules;
$view->payload = $this->buildPayload($request, $forum);
$view->layout = $this->buildLayout();
$baseUrl = array_get($forum, 'data.attributes.baseUrl');
$view->cssUrls = $this->buildCssUrls($baseUrl);
$view->jsUrls = $this->buildJsUrls($baseUrl);
$view->head = $this->buildHeadContent();
$view->foot = $this->buildFootContent(array_get($forum, 'data.attributes.footerHtml'));
return $view->render();
}
protected function buildTitle($forumTitle)
{
return ($this->title ? $this->title.' - ' : '').$forumTitle;
}
protected function buildPayload(Request $request, $forum)
{
$data = $this->getDataFromDocument($forum);
if ($request->getAttribute('actor')->exists) {
$user = $this->getUserDocument($request);
$data = array_merge($data, $this->getDataFromDocument($user));
}
$payload = [
'resources' => $data,
'session' => $this->buildSession($request),
'document' => $this->document,
'locales' => $this->locales->getLocales(),
'locale' => $this->locales->getLocale()
];
return array_merge($payload, $this->variables);
}
protected function buildLayout()
{
$view = $this->view->make($this->layout);
$view->content = $this->buildContent();
return $view;
}
protected function buildContent()
{
$view = $this->view->make('flarum.forum::frontend.content');
$view->content = $this->content;
return $view;
}
protected function buildCssUrls($baseUrl)
{
return $this->buildAssetUrls($baseUrl, [$this->css->getFile(), $this->localeCss->getFile()]);
}
protected function buildJsUrls($baseUrl)
{
return $this->buildAssetUrls($baseUrl, [$this->js->getFile(), $this->localeJs->getFile()]);
}
protected function buildAssetUrls($baseUrl, $files)
{
return array_map(function ($file) use ($baseUrl) {
return $baseUrl.str_replace(public_path(), '', $file);
}, array_filter($files));
}
protected function buildHeadContent()
{
$html = implode("\n", $this->head);
foreach ($this->links as $rel => $href) {
$html .= "\n<link rel=\"$rel\" href=\"$href\" />";
}
return $html;
}
protected function buildFootContent($customFooterHtml)
{
return implode("\n", $this->foot)."\n".$customFooterHtml;
}
/**
* @return CompilerInterface
*/
public function getJs()
{
return $this->js;
}
/**
* @return CompilerInterface
*/
public function getCss()
{
return $this->css;
}
/**
* @return LocaleJsCompiler
*/
public function getLocaleJs()
{
return $this->localeJs;
}
/**
* @return CompilerInterface
*/
public function getLocaleCss()
{
return $this->localeCss;
}
/**
* Get the result of an API request to show the forum.
*
* @param Request $request
* @return array
*/
protected function getForumDocument(Request $request)
{
$actor = $request->getAttribute('actor');
$response = $this->api->send('Flarum\Api\Controller\ShowForumController', $actor);
return json_decode($response->getBody(), true);
}
/**
* Get the result of an API request to show the current user.
*
* @param Request $request
* @return array
*/
protected function getUserDocument(Request $request)
{
$actor = $request->getAttribute('actor');
$this->userSerializer->setActor($actor);
$resource = new Resource($actor, $this->userSerializer);
$document = new Document($resource->with('groups'));
return $document->toArray();
}
/**
* Get information about the current session.
*
* @param Request $request
* @return array
*/
protected function buildSession(Request $request)
{
$actor = $request->getAttribute('actor');
$session = $request->getAttribute('session');
return [
'userId' => $actor->id,
'csrfToken' => $session->token()
];
}
/**
* Get an array of data by merging the 'data' and 'included' keys of a
* JSON-API document.
*
* @param array $document
* @return array
*/
private function getDataFromDocument(array $document)
{
$data[] = $document['data'];
if (isset($document['included'])) {
$data = array_merge($data, $document['included']);
}
return $data;
}
}

View File

@ -1,72 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Api\Client;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\Foundation\Application;
use Flarum\Locale\LocaleManager;
use Illuminate\Contracts\View\Factory;
class FrontendViewFactory
{
/**
* @var Client
*/
protected $api;
/**
* @var Factory
*/
protected $view;
/**
* @var LocaleManager
*/
protected $locales;
/**
* @var CurrentUserSerializer
*/
protected $userSerializer;
/**
* @var Application
*/
protected $app;
/**
* @param Client $api
* @param Factory $view
* @param LocaleManager $locales
* @param CurrentUserSerializer $userSerializer
* @param Application $app
*/
public function __construct(Client $api, Factory $view, LocaleManager $locales, CurrentUserSerializer $userSerializer, Application $app)
{
$this->api = $api;
$this->view = $view;
$this->locales = $locales;
$this->userSerializer = $userSerializer;
$this->app = $app;
}
/**
* @param string $layout
* @param FrontendAssets $assets
* @return FrontendView
*/
public function make($layout, FrontendAssets $assets)
{
return new FrontendView($layout, $assets, $this->api, $this->view, $this->locales, $this->userSerializer, $this->app);
}
}

View File

@ -0,0 +1,251 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
/**
* A view which renders a HTML skeleton for Flarum's frontend app.
*/
class HtmlDocument implements Renderable
{
/**
* The title of the document, displayed in the <title> tag.
*
* @var null|string
*/
public $title;
/**
* The language of the document, displayed as the value of the attribute `lang` in the <html> tag.
*
* @var null|string
*/
public $language;
/**
* The text direction of the document, displayed as the value of the attribute `dir` in the <html> tag.
*
* @var null|string
*/
public $direction;
/**
* The name of the frontend app view to display.
*
* @var string
*/
public $appView = 'flarum::frontend.app';
/**
* The name of the frontend layout view to display.
*
* @var string
*/
public $layoutView;
/**
* The name of the frontend content view to display.
*
* @var string
*/
public $contentView = 'flarum::frontend.content';
/**
* The SEO content of the page, displayed within the layout in <noscript> tags.
*
* @var string|Renderable
*/
public $content;
/**
* Other variables to preload into the Flarum JS.
*
* @var array
*/
public $payload = [];
/**
* An array of meta tags to append to the page's <head>.
*
* @var array
*/
public $meta = [];
/**
* The canonical URL for this page.
*
* This will signal to search engines what URL should be used for this
* content, if it can be found under multiple addresses. This is an
* important tool to tackle duplicate content.
*
* @var null|string
*/
public $canonicalUrl;
/**
* An array of strings to append to the page's <head>.
*
* @var array
*/
public $head = [];
/**
* An array of strings to prepend before the page's </body>.
*
* @var array
*/
public $foot = [];
/**
* An array of JavaScript URLs to load.
*
* @var array
*/
public $js = [];
/**
* An array of CSS URLs to load.
*
* @var array
*/
public $css = [];
/**
* @var Factory
*/
protected $view;
/**
* @var array
*/
protected $forumApiDocument;
/**
* @param Factory $view
* @param array $forumApiDocument
*/
public function __construct(Factory $view, array $forumApiDocument)
{
$this->view = $view;
$this->forumApiDocument = $forumApiDocument;
}
/**
* @return string
*/
public function render(): string
{
$this->view->share('forum', array_get($this->forumApiDocument, 'data.attributes'));
return $this->makeView()->render();
}
/**
* @return View
*/
protected function makeView(): View
{
return $this->view->make($this->appView)->with([
'title' => $this->makeTitle(),
'payload' => $this->payload,
'layout' => $this->makeLayout(),
'language' => $this->language,
'direction' => $this->direction,
'js' => $this->makeJs(),
'head' => $this->makeHead(),
'foot' => $this->makeFoot(),
]);
}
/**
* @return string
*/
protected function makeTitle(): string
{
return ($this->title ? $this->title.' - ' : '').array_get($this->forumApiDocument, 'data.attributes.title');
}
/**
* @return View
*/
protected function makeLayout(): View
{
if ($this->layoutView) {
return $this->view->make($this->layoutView)->with('content', $this->makeContent());
}
}
/**
* @return View
*/
protected function makeContent(): View
{
return $this->view->make($this->contentView)->with('content', $this->content);
}
/**
* @return string
*/
protected function makeHead(): string
{
$head = array_map(function ($url) {
return '<link rel="stylesheet" href="'.e($url).'">';
}, $this->css);
if ($this->canonicalUrl) {
$head[] = '<link rel="canonical" href="'.e($this->canonicalUrl).'">';
}
$head = array_merge($head, array_map(function ($content, $name) {
return '<meta name="'.e($name).'" content="'.e($content).'">';
}, $this->meta, array_keys($this->meta)));
return implode("\n", array_merge($head, $this->head));
}
/**
* @return string
*/
protected function makeJs(): string
{
return implode("\n", array_map(function ($url) {
return '<script src="'.e($url).'"></script>';
}, $this->js));
}
/**
* @return string
*/
protected function makeFoot(): string
{
return implode("\n", $this->foot);
}
/**
* @return array
*/
public function getForumApiDocument(): array
{
return $this->forumApiDocument;
}
/**
* @param array $forumApiDocument
*/
public function setForumApiDocument(array $forumApiDocument)
{
$this->forumApiDocument = $forumApiDocument;
}
}

View File

@ -0,0 +1,180 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Api\Client;
use Flarum\Api\Controller\ShowForumController;
use Flarum\Frontend\Compiler\CompilerInterface;
use Flarum\Frontend\Content\ContentInterface;
use Illuminate\Contracts\View\Factory;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class HtmlDocumentFactory
{
/**
* @var Factory
*/
protected $view;
/**
* @var Client
*/
protected $api;
/**
* @var CompilerFactory
*/
protected $assets;
/**
* @var bool
*/
protected $commitAssets;
/**
* @var ContentInterface[]
*/
protected $content = [];
/**
* @param Factory $view
* @param Client $api
* @param CompilerFactory|null $assets
* @param bool $commitAssets
*/
public function __construct(Factory $view, Client $api, CompilerFactory $assets = null, bool $commitAssets = false)
{
$this->view = $view;
$this->api = $api;
$this->assets = $assets;
$this->commitAssets = $commitAssets;
}
/**
* @param ContentInterface $content
*/
public function add($content)
{
$this->content[] = $content;
}
/**
* @param Request $request
* @return HtmlDocument
*/
public function make(Request $request): HtmlDocument
{
$forumDocument = $this->getForumDocument($request);
$view = new HtmlDocument($this->view, $forumDocument);
$locale = $request->getAttribute('locale');
$js = [$this->assets->makeJs(), $this->assets->makeLocaleJs($locale)];
$css = [$this->assets->makeCss(), $this->assets->makeLocaleCss($locale)];
$this->maybeCommitAssets(array_merge($js, $css));
$view->js = array_merge($view->js, $this->getUrls($js));
$view->css = array_merge($view->css, $this->getUrls($css));
$this->populate($view, $request);
return $view;
}
/**
* @return CompilerFactory
*/
public function getAssets(): CompilerFactory
{
return $this->assets;
}
/**
* @param CompilerFactory $assets
*/
public function setAssets(CompilerFactory $assets)
{
$this->assets = $assets;
}
/**
* @param HtmlDocument $view
* @param Request $request
*/
protected function populate(HtmlDocument $view, Request $request)
{
foreach ($this->content as $content) {
$content->populate($view, $request);
}
}
/**
* @param Request $request
* @return array
*/
private function getForumDocument(Request $request): array
{
$actor = $request->getAttribute('actor');
return $this->getResponseBody(
$this->api->send(ShowForumController::class, $actor)
);
}
/**
* @param Response $response
* @return array
*/
private function getResponseBody(Response $response)
{
return json_decode($response->getBody(), true);
}
private function maybeCommitAssets(array $compilers)
{
if ($this->commitAssets) {
foreach ($compilers as $compiler) {
$compiler->commit();
}
}
}
/**
* @param CompilerInterface[] $compilers
* @return string[]
*/
private function getUrls(array $compilers)
{
return array_filter(array_map(function (CompilerInterface $compiler) {
return $compiler->getUrl();
}, $compilers));
}
/**
* @return bool
*/
public function getCommitAssets(): bool
{
return $this->commitAssets;
}
/**
* @param bool $commitAssets
*/
public function setCommitAssets(bool $commitAssets)
{
$this->commitAssets = $commitAssets;
}
}

View File

@ -0,0 +1,84 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Frontend;
use Flarum\Extension\Event\Disabled;
use Flarum\Extension\Event\Enabled;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Locale\LocaleManager;
use Flarum\Settings\Event\Saved;
use Illuminate\Contracts\Events\Dispatcher;
class RecompileFrontendAssets
{
/**
* @var CompilerFactory
*/
protected $assets;
/**
* @var LocaleManager
*/
protected $locales;
/**
* @param CompilerFactory $assets
* @param LocaleManager $locales
*/
public function __construct(CompilerFactory $assets, LocaleManager $locales)
{
$this->assets = $assets;
$this->locales = $locales;
}
/**
* @param Dispatcher $events
*/
public function subscribe(Dispatcher $events)
{
$events->listen(Saved::class, [$this, 'whenSettingsSaved']);
$events->listen(Enabled::class, [$this, 'flush']);
$events->listen(Disabled::class, [$this, 'flush']);
$events->listen(ClearingCache::class, [$this, 'flush']);
}
public function whenSettingsSaved(Saved $event)
{
if (preg_grep('/^theme_/i', array_keys($event->settings))) {
$this->flushCss();
}
}
public function flush()
{
$this->flushCss();
$this->flushJs();
}
protected function flushCss()
{
$this->assets->makeCss()->flush();
foreach ($this->locales->getLocales() as $locale => $name) {
$this->assets->makeLocaleCss($locale)->flush();
}
}
protected function flushJs()
{
$this->assets->makeJs()->flush();
foreach ($this->locales->getLocales() as $locale => $name) {
$this->assets->makeLocaleJs($locale)->flush();
}
}
}

View File

@ -25,13 +25,13 @@ class ControllerRouteHandler
protected $container;
/**
* @var string
* @var string|callable
*/
protected $controller;
/**
* @param Container $container
* @param string $controller
* @param string|callable $controller
*/
public function __construct(Container $container, $controller)
{
@ -54,12 +54,16 @@ class ControllerRouteHandler
}
/**
* @param string $class
* @param string|callable $class
* @return RequestHandlerInterface
*/
protected function resolveController($class)
{
$controller = $this->container->make($class);
if (is_callable($class)) {
$controller = $this->container->call($class);
} else {
$controller = $this->container->make($class);
}
if (! ($controller instanceof RequestHandlerInterface)) {
throw new InvalidArgumentException(

View File

@ -46,6 +46,8 @@ class SetLocale implements Middleware
$this->locales->setLocale($locale);
}
$request = $request->withAttribute('locale', $this->locales->getLocale());
return $handler->handle($request);
}
}

View File

@ -11,6 +11,7 @@
namespace Flarum\Http;
use Flarum\Frontend\Controller as FrontendController;
use Illuminate\Contracts\Container\Container;
class RouteHandlerFactory
@ -29,11 +30,47 @@ class RouteHandlerFactory
}
/**
* @param string $controller
* @param string|callable $controller
* @return ControllerRouteHandler
*/
public function toController($controller)
{
return new ControllerRouteHandler($this->container, $controller);
}
/**
* @param string $frontend
* @param string|null $content
* @return ControllerRouteHandler
*/
public function toFrontend(string $frontend, string $content = null)
{
return $this->toController(function (Container $container) use ($frontend, $content) {
$frontend = $container->make($frontend);
if ($content) {
$frontend->add($container->make($content));
}
return new FrontendController($frontend);
});
}
/**
* @param string|null $content
* @return ControllerRouteHandler
*/
public function toForum(string $content = null)
{
return $this->toFrontend('flarum.forum.frontend', $content);
}
/**
* @param string|null $content
* @return ControllerRouteHandler
*/
public function toAdmin(string $content = null)
{
return $this->toFrontend('flarum.admin.frontend', $content);
}
}

View File

@ -25,7 +25,7 @@ class LocaleServiceProvider extends AbstractServiceProvider
*/
public function boot(Dispatcher $events)
{
$locales = $this->app->make('flarum.localeManager');
$locales = $this->app->make('flarum.locales');
$locales->addLocale($this->getDefaultLocale(), 'Default');
@ -38,7 +38,7 @@ class LocaleServiceProvider extends AbstractServiceProvider
public function register()
{
$this->app->singleton(LocaleManager::class);
$this->app->alias(LocaleManager::class, 'flarum.localeManager');
$this->app->alias(LocaleManager::class, 'flarum.locales');
$this->app->singleton('translator', function () {
$translator = new Translator($this->getDefaultLocale(), new MessageSelector());

View File

@ -14,26 +14,15 @@ namespace Flarum\Settings\Event;
class Saved
{
/**
* The setting key that was set.
*
* @var string
* @var array
*/
public $key;
public $settings;
/**
* The setting value that was set.
*
* @var string
* @param array $settings
*/
public $value;
/**
* @param string $key The setting key that was set.
* @param string $value The setting value that was set.
*/
public function __construct($key, $value)
public function __construct(array $settings)
{
$this->key = $key;
$this->value = $value;
$this->settings = $settings;
}
}

View File

@ -9,17 +9,20 @@
* file that was distributed with this source code.
*/
namespace Flarum\Admin;
namespace Flarum\Settings\Event;
use Flarum\Frontend\AbstractFrontend;
class Frontend extends AbstractFrontend
class Saving
{
/**
* {@inheritdoc}
* @var array
*/
protected function getName()
public $settings;
/**
* @param array $settings
*/
public function __construct(array &$settings)
{
return 'admin';
$this->settings = &$settings;
}
}

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Settings;
/**
* A settings repository decorator that allows overriding certain values.
*
* The `OverrideSettingsRepository` class decorates another
* `SettingsRepositoryInterface` instance but allows certain settings to be
* overridden with predefined values. It does not affect writing methods.
*
* Within Flarum, this can be used to test out new setting values in a system
* before they are committed to the database.
*
* @see \Flarum\Forum\RecompileFrontendAssets For an example usage.
*/
class OverrideSettingsRepository implements SettingsRepositoryInterface
{
protected $inner;
protected $overrides = [];
public function __construct(SettingsRepositoryInterface $inner, array $overrides)
{
$this->inner = $inner;
$this->overrides = $overrides;
}
public function all()
{
return array_merge($this->inner->all(), $this->overrides);
}
public function get($key, $default = null)
{
if (array_key_exists($key, $this->overrides)) {
return $this->overrides[$key];
}
return array_get($this->all(), $key, $default);
}
public function set($key, $value)
{
$this->inner->set($key, $value);
}
public function delete($key)
{
$this->inner->delete($key);
}
}

View File

@ -1,37 +1,37 @@
<div id="app" class="App">
<div id="app-navigation" class="App-navigation"></div>
<div id="app-navigation" class="App-navigation"></div>
<div id="drawer" class="App-drawer">
<div id="drawer" class="App-drawer">
<header id="header" class="App-header">
<div id="header-navigation" class="Header-navigation"></div>
<div class="container">
<h1 class="Header-title">
<a href="{{ array_get($forum, 'attributes.baseUrl') }}">
<?php $title = array_get($forum, 'attributes.title'); ?>
@if ($logo = array_get($forum, 'attributes.logoUrl'))
<img src="{{ $logo }}" alt="{{ $title }}" class="Header-logo">
@else
{{ $title }}
@endif
</a>
</h1>
<div id="header-primary" class="Header-primary"></div>
<div id="header-secondary" class="Header-secondary"></div>
</div>
</header>
<header id="header" class="App-header">
<div id="header-navigation" class="Header-navigation"></div>
<div class="container">
<h1 class="Header-title">
<a href="{{ array_get($forum, 'baseUrl') }}">
<?php $title = array_get($forum, 'title'); ?>
@if ($logo = array_get($forum, 'logoUrl'))
<img src="{{ $logo }}" alt="{{ $title }}" class="Header-logo">
@else
{{ $title }}
@endif
</a>
</h1>
<div id="header-primary" class="Header-primary"></div>
<div id="header-secondary" class="Header-secondary"></div>
</div>
</header>
</div>
<main class="App-content">
<div class="container">
<div id="admin-navigation" class="App-nav sideNav"></div>
</div>
<div id="content" class="sideNavOffset"></div>
<main class="App-content">
<div class="container">
<div id="admin-navigation" class="App-nav sideNav"></div>
</div>
{!! $content !!}
</main>
<div id="content" class="sideNavOffset"></div>
{!! $content !!}
</main>
</div>

View File

@ -1,63 +1,40 @@
<!doctype html>
<html dir="{{ $direction }}" lang="{{ $language }}">
<head>
<meta charset="utf-8">
<title>{{ $title }}</title>
<meta name="description" content="{{ $description }}">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<meta name="theme-color" content="{{ array_get($forum, 'attributes.themePrimaryColor') }}">
@if (! $allowJs)
<meta name="robots" content="noindex" />
@endif
<head>
<meta charset="utf-8">
<title>{{ $title }}</title>
@foreach ($cssUrls as $url)
<link rel="stylesheet" href="{{ $url }}">
@endforeach
{!! $head !!}
</head>
@if ($faviconUrl = array_get($forum, 'attributes.faviconUrl'))
<link href="{{ $faviconUrl }}" rel="shortcut icon">
@endif
<body>
{!! $layout !!}
{!! $head !!}
</head>
<div id="modal"></div>
<div id="alerts"></div>
<body>
{!! $layout !!}
<script>
document.getElementById('flarum-loading').style.display = 'block';
var flarum = {extensions: {}};
</script>
<div id="modal"></div>
<div id="alerts"></div>
{!! $js !!}
@if ($allowJs)
<script>
document.getElementById('flarum-loading').style.display = 'block';
var flarum = {extensions: {}};
</script>
<script>
document.getElementById('flarum-loading').style.display = 'none';
@foreach ($jsUrls as $url)
<script src="{{ $url }}"></script>
@endforeach
try {
flarum.core.app.load(@json($payload));
flarum.core.app.bootExtensions(flarum.extensions);
flarum.core.app.boot();
} catch (e) {
var error = document.getElementById('flarum-loading-error');
error.innerHTML += document.getElementById('flarum-content').textContent;
error.style.display = 'block';
throw e;
}
</script>
<script>
document.getElementById('flarum-loading').style.display = 'none';
@if (! $debug)
try {
@endif
flarum.core.app.load(@json($payload));
flarum.core.app.bootExtensions(flarum.extensions);
flarum.core.app.boot();
@if (! $debug)
} catch (e) {
window.location += (window.location.search ? '&' : '?') + 'nojs=1';
throw e;
}
@endif
</script>
@else
<script>
window.history.replaceState(null, null, window.location.toString().replace(/([&?]nojs=1$|nojs=1&)/, ''));
</script>
@endif
{!! $foot !!}
</body>
{!! $foot !!}
</body>
</html>

View File

@ -1,23 +1,23 @@
<div id="flarum-loading" style="display: none">
{{ $translator->trans('core.views.content.loading_text') }}
{{ $translator->trans('core.views.content.loading_text') }}
</div>
@if ($allowJs)
<noscript>
<noscript>
<div class="Alert">
<div class="container">
{{ $translator->trans('core.views.content.javascript_disabled_message') }}
</div>
<div class="container">
{{ $translator->trans('core.views.content.javascript_disabled_message') }}
</div>
</div>
</noscript>
<div id="flarum-loading-error" style="display: none">
<div class="Alert">
<div class="container">
{{ $translator->trans('core.views.content.load_error_message') }}
</div>
</div>
</div>
<noscript id="flarum-content">
{!! $content !!}
</noscript>
@else
<div class="Alert Alert--error">
<div class="container">
{{ $translator->trans('core.views.content.load_error_message') }}
</div>
</div>
{!! $content !!}
@endif
</noscript>

View File

@ -1,5 +1,5 @@
<div class="container">
<h2>{{ $document->data->attributes->title }}</h2>
<h2>{{ $apiDocument->data->attributes->title }}</h2>
<div>
@foreach ($posts as $post)
@ -15,11 +15,11 @@
@endforeach
</div>
@if (isset($document->links->prev))
@if (isset($apiDocument->links->prev))
<a href="{{ $url(['page' => $page - 1]) }}">&laquo; {{ $translator->trans('core.views.discussion.previous_page_button') }}</a>
@endif
@if (isset($document->links->next))
@if (isset($apiDocument->links->next))
<a href="{{ $url(['page' => $page + 1]) }}">{{ $translator->trans('core.views.discussion.next_page_button') }} &raquo;</a>
@endif
</div>

View File

@ -4,7 +4,7 @@
<h2>{{ $translator->trans('core.views.index.all_discussions_heading') }}</h2>
<ul>
@foreach ($document->data as $discussion)
@foreach ($apiDocument->data as $discussion)
<li>
<a href="{{ $url->to('forum')->route('discussion', [
'id' => $discussion->id . (trim($discussion->attributes->slug) ? '-' . $discussion->attributes->slug : '')
@ -15,11 +15,11 @@
@endforeach
</ul>
@if (isset($document->links->prev))
@if (isset($apiDocument->links->prev))
<a href="{{ $url->to('forum')->route('index') }}?page={{ $page - 1 }}">&laquo; {{ $translator->trans('core.views.index.previous_page_button') }}</a>
@endif
@if (isset($document->links->next))
@if (isset($apiDocument->links->next))
<a href="{{ $url->to('forum')->route('index') }}?page={{ $page + 1 }}">{{ $translator->trans('core.views.index.next_page_button') }} &raquo;</a>
@endif
</div>

View File

@ -1,64 +1,42 @@
<?php
/**
* Forum Web App Template
*
* NOTE: You shouldn't edit this file directly. Your changes will be overwritten
* when you update Flarum. See flarum.org/docs/templates to learn how to
* customize your forum's layout.
*
* Flarum's JavaScript app mounts various components into key elements in
* this template. They are distinguished by their ID attributes:
*
* - #app
* - #app-navigation
* - #drawer
* - #header
* - #header-navigation
* - #home-link
* - #header-primary
* - #header-secondary
* - #content
* - #composer
*/
?>
{!! array_get($forum, 'attributes.headerHtml') !!}
{!! array_get($forum, 'headerHtml') !!}
<div id="app" class="App">
<div id="app-navigation" class="App-navigation"></div>
<div id="app-navigation" class="App-navigation"></div>
<div id="drawer" class="App-drawer">
<div id="drawer" class="App-drawer">
<header id="header" class="App-header">
<div id="header-navigation" class="Header-navigation"></div>
<div class="container">
<h1 class="Header-title">
<a href="{{ array_get($forum, 'attributes.baseUrl') }}" id="home-link">
<?php $title = array_get($forum, 'attributes.title'); ?>
@if ($logo = array_get($forum, 'attributes.logoUrl'))
<img src="{{ $logo }}" alt="{{ $title }}" class="Header-logo">
@else
{{ $title }}
@endif
</a>
</h1>
<div id="header-primary" class="Header-primary"></div>
<div id="header-secondary" class="Header-secondary"></div>
</div>
</header>
<header id="header" class="App-header">
<div id="header-navigation" class="Header-navigation"></div>
<div class="container">
<h1 class="Header-title">
<a href="{{ array_get($forum, 'baseUrl') }}" id="home-link">
@if ($logo = array_get($forum, 'logoUrl'))
<img src="{{ $logo }}" alt="{{ array_get($forum, 'title') }}" class="Header-logo">
@else
{{ array_get($forum, 'title') }}
@endif
</a>
</h1>
<div id="header-primary" class="Header-primary"></div>
<div id="header-secondary" class="Header-secondary"></div>
</div>
</header>
</div>
<main class="App-content">
<div id="content"></div>
{!! $content !!}
<div class="App-composer">
<div class="container">
<div id="composer"></div>
</div>
</div>
</main>
<main class="App-content">
<div id="content"></div>
{!! $content !!}
<div class="App-composer">
<div class="container">
<div id="composer"></div>
</div>
</div>
</main>
</div>
{!! array_get($forum, 'footerHtml') !!}