AzuraCast/src/View.php

313 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App;
use App\Entity\Station;
use App\Entity\User;
use App\Enums\SupportedLocales;
use App\Http\RouterInterface;
use App\Http\ServerRequest;
use App\Traits\RequestAwareTrait;
use App\Utilities\Json;
use App\View\GlobalSections;
use Doctrine\Common\Collections\ArrayCollection;
use League\Plates\Engine;
use League\Plates\Template\Data;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use stdClass;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
final class View extends Engine
{
use RequestAwareTrait;
private GlobalSections $sections;
/** @var ArrayCollection<string, array|object|string|int|bool> */
private ArrayCollection $globalProps;
public function __construct(
Customization $customization,
Environment $environment,
EventDispatcherInterface $dispatcher,
Version $version,
RouterInterface $router
) {
parent::__construct(
dirname(__DIR__) . '/templates',
'phtml'
);
$this->sections = new GlobalSections();
$this->globalProps = new ArrayCollection();
// Add non-request-dependent content.
$this->addData(
[
'sections' => $this->sections,
'globalProps' => $this->globalProps,
'customization' => $customization,
'environment' => $environment,
'version' => $version,
'router' => $router,
]
);
$this->registerFunction(
'escapeJs',
function ($string) {
return json_encode($string, JSON_THROW_ON_ERROR);
}
);
$this->registerFunction(
'dump',
function ($value) {
if (class_exists(VarCloner::class)) {
$varCloner = new VarCloner();
$dumpedValue = (new CliDumper())->dump($varCloner->cloneVar($value), true);
} else {
$dumpedValue = json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
}
return '<pre>' . htmlspecialchars($dumpedValue ?? '', ENT_QUOTES | ENT_HTML5) . '</pre>';
}
);
$vueComponents = (!$environment->isDevelopment())
? Json::loadFromFile($environment->getBaseDirectory() . '/web/static/vite_dist/.vite/manifest.json')
: [];
$this->registerFunction(
'getVueComponentInfo',
function (string $componentPath) use ($vueComponents, $environment) {
$assetRoot = '/static/vite_dist';
if ($environment->isDevelopment() || $environment->isTesting()) {
return [
'js' => $assetRoot . '/' . $componentPath,
'css' => [],
'prefetch' => [],
];
}
if (!isset($vueComponents[$componentPath])) {
return null;
}
$includes = [
'js' => $assetRoot . '/' . $vueComponents[$componentPath]['file'],
'css' => [],
'prefetch' => [],
];
$visitedNodes = [];
$fetchCss = function ($component) use (
$vueComponents,
$assetRoot,
&$includes,
&$fetchCss,
&$visitedNodes
): void {
if (!isset($vueComponents[$component]) || isset($visitedNodes[$component])) {
return;
}
$visitedNodes[$component] = true;
$componentInfo = $vueComponents[$component];
if (isset($componentInfo['css'])) {
foreach ($componentInfo['css'] as $css) {
$includes['css'][] = $assetRoot . '/' . $css;
}
}
if (isset($componentInfo['file'])) {
$fileUrl = $assetRoot . '/' . $componentInfo['file'];
if ($fileUrl !== $includes['js']) {
$includes['prefetch'][] = $fileUrl;
}
}
if (isset($componentInfo['imports'])) {
foreach ($componentInfo['imports'] as $import) {
$fetchCss($import);
}
}
};
$fetchCss($componentPath);
return $includes;
}
);
$dispatcher->dispatch(new Event\BuildView($this));
}
public function setRequest(?ServerRequest $request): void
{
$this->request = $request;
if (null !== $request) {
$requestData = [
'request' => $request,
'auth' => $request->getAttribute(ServerRequest::ATTR_AUTH),
'acl' => $request->getAttribute(ServerRequest::ATTR_ACL),
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
];
$router = $request->getAttribute(ServerRequest::ATTR_ROUTER);
if (null !== $router) {
$requestData['router'] = $router;
}
$customization = $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION);
if ($customization instanceof Customization) {
$requestData['customization'] = $customization;
$this->globalProps->set(
'enableAdvancedFeatures',
$customization->enableAdvancedFeatures()
);
}
$localeObj = $request->getAttribute(ServerRequest::ATTR_LOCALE);
if (!($localeObj instanceof SupportedLocales)) {
$localeObj = SupportedLocales::default();
}
$locale = $localeObj->getLocaleWithoutEncoding();
$localeShort = substr($locale, 0, 2);
$localeWithDashes = str_replace('_', '-', $locale);
$this->globalProps->set('locale', $locale);
$this->globalProps->set('localeShort', $localeShort);
$this->globalProps->set('localeWithDashes', $localeWithDashes);
// User profile-specific 24-hour display setting.
$userObj = $request->getAttribute(ServerRequest::ATTR_USER);
$requestData['user'] = $userObj;
$timeConfig = new stdClass();
if ($userObj instanceof User) {
$timeConfig->hour12 = !$userObj->getShow24HourTime();
$globalPermissions = [];
$stationPermissions = [];
foreach ($userObj->getRoles() as $role) {
foreach ($role->getPermissions() as $permission) {
$station = $permission->getStation();
if (null !== $station) {
$stationPermissions[$station->getIdRequired()][] = $permission->getActionName();
} else {
$globalPermissions[] = $permission->getActionName();
}
}
}
$this->globalProps->set('user', [
'id' => $userObj->getIdRequired(),
'displayName' => $userObj->getDisplayName(),
'globalPermissions' => $globalPermissions,
'stationPermissions' => $stationPermissions,
]);
}
$this->globalProps->set('timeConfig', $timeConfig);
// Station-specific properties
$station = $request->getAttribute(ServerRequest::ATTR_STATION);
if ($station instanceof Station) {
$this->globalProps->set('station', [
'id' => $station->getIdRequired(),
'name' => $station->getName(),
'isEnabled' => $station->getIsEnabled(),
'shortName' => $station->getShortName(),
'timezone' => $station->getTimezone(),
'offlineText' => $station->getBrandingConfig()->getOfflineText(),
]);
}
$this->addData($requestData);
}
}
public function getSections(): GlobalSections
{
return $this->sections;
}
/** @return ArrayCollection<string, array|object|string|int|bool> */
public function getGlobalProps(): ArrayCollection
{
return $this->globalProps;
}
public function reset(): void
{
$this->sections = new GlobalSections();
$this->globalProps = new ArrayCollection();
$this->data = new Data();
}
/**
* @param string $name
* @param array $data
*/
public function fetch(string $name, array $data = []): string
{
return $this->render($name, $data);
}
/**
* Trigger rendering of template and write it directly to the PSR-7 compatible Response object.
*
* @param ResponseInterface $response
* @param string $templateName
* @param array $templateArgs
*/
public function renderToResponse(
ResponseInterface $response,
string $templateName,
array $templateArgs = []
): ResponseInterface {
$response->getBody()->write(
$this->render($templateName, $templateArgs)
);
return $response->withHeader('Content-type', 'text/html; charset=utf-8');
}
public function renderVuePage(
ResponseInterface $response,
string $component,
?string $id = null,
string $layout = 'panel',
?string $title = null,
array $layoutParams = [],
array $props = [],
): ResponseInterface {
$id ??= $component;
return $this->renderToResponse(
$response,
'system/vue_page',
[
'component' => $component,
'id' => $id,
'layout' => $layout,
'title' => $title,
'layoutParams' => $layoutParams,
'props' => $props,
]
);
}
}