1
0
mirror of https://github.com/flarum/core.git synced 2025-07-24 02:01:19 +02:00

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

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