julienmonnerie/kirby/src/Panel/Document.php

294 lines
7.3 KiB
PHP
Raw Normal View History

2022-06-17 17:51:59 +02:00
<?php
namespace Kirby\Panel;
2022-08-31 15:02:43 +02:00
use Kirby\Cms\App;
2022-06-17 17:51:59 +02:00
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
2022-08-31 15:02:43 +02:00
use Kirby\Filesystem\Asset;
2022-06-17 17:51:59 +02:00
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Http\Response;
use Kirby\Http\Uri;
use Kirby\Toolkit\Tpl;
use Throwable;
/**
* The Document is used by the View class to render
* the full Panel HTML document in Fiber calls that
* should not return just JSON objects
* @since 3.6.0
*
* @package Kirby Panel
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Document
{
2022-08-31 15:02:43 +02:00
/**
* Generates an array with all assets
* that need to be loaded for the panel (js, css, icons)
*/
public static function assets(): array
{
$kirby = App::instance();
$nonce = $kirby->nonce();
// get the assets from the Vite dev server in dev mode;
// dev mode = explicitly enabled in the config AND Vite is running
$dev = $kirby->option('panel.dev', false);
$isDev = $dev !== false && is_file($kirby->roots()->panel() . '/.vite-running') === true;
if ($isDev === true) {
// vite on explicitly configured base URL or port 3000
// of the current Kirby request
if (is_string($dev) === true) {
$url = $dev;
} else {
$url = rtrim($kirby->request()->url([
'port' => 3000,
'path' => null,
'params' => null,
'query' => null
])->toString(), '/');
}
} else {
// vite is not running, use production assets
$url = $kirby->url('media') . '/panel/' . $kirby->versionHash();
}
// fetch all plugins
$plugins = new Plugins();
$assets = [
'css' => [
'index' => $url . '/css/style.css',
'plugins' => $plugins->url('css'),
'custom' => static::customAsset('panel.css'),
],
'icons' => static::favicon($url),
2024-12-20 12:37:52 +01:00
// loader for plugins' index.dev.mjs files
// inlined, so we provide the code instead of the asset URL
2022-08-31 15:02:43 +02:00
'plugin-imports' => $plugins->read('mjs'),
'js' => [
2024-12-20 12:37:52 +01:00
'vue' => [
'nonce' => $nonce,
'src' => $url . '/js/vue.js'
],
'vendor' => [
2022-08-31 15:02:43 +02:00
'nonce' => $nonce,
'src' => $url . '/js/vendor.js',
'type' => 'module'
],
'pluginloader' => [
'nonce' => $nonce,
'src' => $url . '/js/plugins.js',
'type' => 'module'
],
2024-12-20 12:37:52 +01:00
'plugins' => [
2022-08-31 15:02:43 +02:00
'nonce' => $nonce,
'src' => $plugins->url('js'),
'defer' => true
],
2024-12-20 12:37:52 +01:00
'custom' => [
2022-08-31 15:02:43 +02:00
'nonce' => $nonce,
'src' => static::customAsset('panel.js'),
'type' => 'module'
],
2024-12-20 12:37:52 +01:00
'index' => [
2022-08-31 15:02:43 +02:00
'nonce' => $nonce,
'src' => $url . '/js/index.js',
'type' => 'module'
],
]
];
// during dev mode, add vite client and adapt
// path to `index.js` - vendor and stylesheet
// don't need to be loaded in dev mode
if ($isDev === true) {
$assets['js']['vite'] = [
'nonce' => $nonce,
'src' => $url . '/@vite/client',
'type' => 'module'
];
$assets['js']['index'] = [
'nonce' => $nonce,
'src' => $url . '/src/index.js',
'type' => 'module'
];
2024-12-20 12:37:52 +01:00
// load the development version of Vue
$assets['js']['vue']['src'] = $url . '/node_modules/vue/dist/vue.js';
2022-08-31 15:02:43 +02:00
unset($assets['css']['index'], $assets['js']['vendor']);
}
// remove missing files
$assets['css'] = array_filter($assets['css']);
$assets['js'] = array_filter(
$assets['js'],
fn ($js) => empty($js['src']) === false
);
return $assets;
}
/**
* Check for a custom asset file from the
* config (e.g. panel.css or panel.js)
* @since 3.7.0
*
* @param string $option asset option name
*/
2022-12-19 14:56:05 +01:00
public static function customAsset(string $option): string|null
2022-08-31 15:02:43 +02:00
{
if ($path = App::instance()->option($option)) {
$asset = new Asset($path);
if ($asset->exists() === true) {
return $asset->url() . '?' . $asset->modified();
}
}
return null;
}
/**
2022-12-19 14:56:05 +01:00
* Returns array of favicon icons
2022-08-31 15:02:43 +02:00
* based on config option
* @since 3.7.0
*
* @param string $url URL prefix for default icons
2022-12-19 14:56:05 +01:00
* @throws \Kirby\Exception\InvalidArgumentException
2022-08-31 15:02:43 +02:00
*/
public static function favicon(string $url = ''): array
{
$kirby = App::instance();
$icons = $kirby->option('panel.favicon', [
'apple-touch-icon' => [
'type' => 'image/png',
'url' => $url . '/apple-touch-icon.png',
],
'alternate icon' => [
'type' => 'image/png',
'url' => $url . '/favicon.png',
2022-12-19 14:56:05 +01:00
],
'shortcut icon' => [
'type' => 'image/svg+xml',
'url' => $url . '/favicon.svg',
2022-08-31 15:02:43 +02:00
]
]);
if (is_array($icons) === true) {
return $icons;
}
// make sure to convert favicon string to array
if (is_string($icons) === true) {
return [
'shortcut icon' => [
'type' => F::mime($icons),
'url' => $icons,
]
];
}
throw new InvalidArgumentException('Invalid panel.favicon option');
}
/**
* Load the SVG icon sprite
* This will be injected in the
* initial HTML document for the Panel
*/
public static function icons(): string
{
2023-04-14 16:34:06 +02:00
$dev = App::instance()->option('panel.dev', false);
$dir = $dev ? 'public' : 'dist';
return F::read(App::instance()->root('kirby') . '/panel/' . $dir . '/img/icons.svg');
2022-08-31 15:02:43 +02:00
}
/**
* Links all dist files in the media folder
* and returns the link to the requested asset
*
* @throws \Kirby\Exception\Exception If Panel assets could not be moved to the public directory
*/
public static function link(): bool
{
$kirby = App::instance();
$mediaRoot = $kirby->root('media') . '/panel';
$panelRoot = $kirby->root('panel') . '/dist';
$versionHash = $kirby->versionHash();
$versionRoot = $mediaRoot . '/' . $versionHash;
// check if the version already exists
if (is_dir($versionRoot) === true) {
return false;
}
// delete the panel folder and all previous versions
Dir::remove($mediaRoot);
// recreate the panel folder
Dir::make($mediaRoot, true);
// copy assets to the dist folder
if (Dir::copy($panelRoot, $versionRoot) !== true) {
throw new Exception('Panel assets could not be linked');
}
return true;
}
/**
* Renders the panel document
*/
2022-12-19 14:56:05 +01:00
public static function response(array $fiber): Response
2022-08-31 15:02:43 +02:00
{
$kirby = App::instance();
// Full HTML response
// @codeCoverageIgnoreStart
try {
if (static::link() === true) {
usleep(1);
Response::go($kirby->url('base') . '/' . $kirby->path());
}
} catch (Throwable $e) {
die('The Panel assets cannot be installed properly. ' . $e->getMessage());
}
// @codeCoverageIgnoreEnd
// get the uri object for the panel url
$uri = new Uri($url = $kirby->url('panel'));
// proper response code
$code = $fiber['$view']['code'] ?? 200;
// load the main Panel view template
$body = Tpl::load($kirby->root('kirby') . '/views/panel.php', [
'assets' => static::assets(),
'icons' => static::icons(),
'nonce' => $kirby->nonce(),
'fiber' => $fiber,
'panelUrl' => $uri->path()->toString(true) . '/',
]);
2024-12-20 12:37:52 +01:00
$frameAncestors = $kirby->option('panel.frameAncestors');
$frameAncestors = match (true) {
$frameAncestors === true => "'self'",
is_array($frameAncestors) => "'self' " . implode(' ', $frameAncestors),
is_string($frameAncestors) => $frameAncestors,
default => "'none'"
};
return new Response($body, 'text/html', $code, [
'Content-Security-Policy' => 'frame-ancestors ' . $frameAncestors
]);
2022-08-31 15:02:43 +02:00
}
2022-06-17 17:51:59 +02:00
}