digital_signage_framework-2.3.x-dev/src/Controller/Api.php
src/Controller/Api.php
<?php
namespace Drupal\digital_signage_framework\Controller;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\LibraryDiscoveryInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Http\ClientFactory;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\digital_signage_framework\DeviceInterface;
use Drupal\digital_signage_framework\DigitalSignageFrameworkEvents;
use Drupal\digital_signage_framework\Emergency;
use Drupal\digital_signage_framework\Entity\Device;
use Drupal\digital_signage_framework\Event\Libraries;
use Drupal\digital_signage_framework\Event\Overlays;
use Drupal\digital_signage_framework\Event\Rendered;
use Drupal\digital_signage_framework\Event\Underlays;
use Drupal\digital_signage_framework\PlatformInterface;
use Drupal\digital_signage_framework\Renderer;
use Drupal\digital_signage_framework\ScheduleInterface;
use Drupal\file\Entity\File;
use Drupal\media\Entity\Media;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use function Jawira\PlantUml\encodep;
/**
* Provides the API controller.
*/
class Api implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
*/
protected LibraryDiscoveryInterface $libraryDiscovery;
/**
* The request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected Request $request;
/**
* The config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected ImmutableConfig $config;
/**
* The device.
*
* @var \Drupal\digital_signage_framework\DeviceInterface|null
*/
protected ?DeviceInterface $device = NULL;
/**
* The schedule.
*
* @var \Drupal\digital_signage_framework\ScheduleInterface
*/
protected ScheduleInterface $schedule;
/**
* The platform.
*
* @var \Drupal\digital_signage_framework\PlatformInterface
*/
protected PlatformInterface $platform;
/**
* The renderer service.
*
* @var \Drupal\digital_signage_framework\Renderer
*/
protected Renderer $dsRenderer;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* The http client.
*
* @var \Drupal\Core\Http\ClientFactory
*/
protected ClientFactory $clientFactory;
/**
* The core renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected AccountProxyInterface $currentUser;
/**
* The emergency service.
*
* @var \Drupal\digital_signage_framework\Emergency
*/
protected Emergency $emergency;
/**
* The asset resolver.
*
* @var \Drupal\Core\Asset\AssetResolverInterface
*/
protected AssetResolverInterface $assetResolver;
/**
* The js asset collection renderer.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected AssetCollectionRendererInterface $jsAssetCollectionRenderer;
/**
* The css asset collection renderer.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected AssetCollectionRendererInterface $cssAssetCollectionRenderer;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected LanguageManagerInterface $languageManager;
/**
* The theme.
*
* @var string
*/
protected string $theme;
/**
* The list of module extensions.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* The file url generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected FileUrlGeneratorInterface $fileUrlGenerator;
/**
* Api constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
* The library discovery service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\digital_signage_framework\Renderer $ds_renderer
* The renderer service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Http\ClientFactory $client_factory
* The http client.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The core renderer.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
* @param \Drupal\digital_signage_framework\Emergency $emergency
* The emergency service.
* @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
* The asset resolver.
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_asset_collection_renderer
* The js asset collection renderer.
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_asset_collection_renderer
* The css asset collection renderer.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
* The list of module extensions.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
* The file url generator.
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, LibraryDiscoveryInterface $library_discovery, RequestStack $request_stack, Renderer $ds_renderer, EventDispatcherInterface $event_dispatcher, ClientFactory $client_factory, RendererInterface $renderer, AccountProxyInterface $current_user, Emergency $emergency, AssetResolverInterface $asset_resolver, AssetCollectionRendererInterface $js_asset_collection_renderer, AssetCollectionRendererInterface $css_asset_collection_renderer, LanguageManagerInterface $language_manager, ModuleExtensionList $moduleExtensionList, FileUrlGeneratorInterface $fileUrlGenerator) {
$this->config = $config_factory->get('digital_signage_framework.settings');
$this->theme = $config_factory->get('system.theme')->get('default');
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->libraryDiscovery = $library_discovery;
$this->request = $request_stack->getCurrentRequest();
$this->dsRenderer = $ds_renderer;
$this->eventDispatcher = $event_dispatcher;
$this->clientFactory = $client_factory;
$this->renderer = $renderer;
$this->currentUser = $current_user;
$this->emergency = $emergency;
// We need to load the device first, otherwise we can't verify fingerprint.
if ($deviceId = $this->request->query->get('deviceId')) {
$this->device = Device::load($deviceId);
}
if (!$current_user->hasPermission('digital signage framework access preview') && $this->request->headers->get('x-digsig-fingerprint') === NULL) {
return;
}
if ($this->device !== NULL) {
$this->platform = $this->device->getPlugin();
$stored = $this->request->query->get('storedSchedule', 'true') === 'true';
$this->schedule = $this->device->getSchedule($stored);
}
$this->assetResolver = $asset_resolver;
$this->jsAssetCollectionRenderer = $js_asset_collection_renderer;
$this->cssAssetCollectionRenderer = $css_asset_collection_renderer;
$this->languageManager = $language_manager;
$this->moduleExtensionList = $moduleExtensionList;
$this->fileUrlGenerator = $fileUrlGenerator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): Api {
return new Api(
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('library.discovery'),
$container->get('request_stack'),
$container->get('digital_signage_framework.renderer'),
$container->get('event_dispatcher'),
$container->get('http_client_factory'),
$container->get('renderer'),
$container->get('current_user'),
$container->get('digital_signage_content_setting.emergency'),
$container->get('asset.resolver'),
$container->get('asset.js.collection_renderer'),
$container->get('asset.css.collection_renderer'),
$container->get('language_manager'),
$container->get('extension.list.module'),
$container->get('file_url_generator')
);
}
/**
* Gets the device specific fingerprint.
*
* @param \Drupal\digital_signage_framework\DeviceInterface $device
* The device.
*
* @return string
* The fingerprint.
*/
public static function fingerprint(DeviceInterface $device): string {
return Crypt::hmacBase64($device->extId(), Settings::getHashSalt() . $device->id());
}
/**
* Verifies access to the device.
*
* @return \Drupal\Core\Access\AccessResult
* The access result.
*/
public function access(): AccessResult {
if (empty($this->device)) {
return AccessResult::forbidden('Missing or broken device definition.');
}
if (!$this->currentUser->hasPermission('digital signage framework access preview') && $this->request->headers->get('x-digsig-fingerprint') !== self::fingerprint($this->device)) {
return AccessResult::forbidden('Wrong fingerprint.');
}
switch ($this->request->query->get('mode')) {
case 'load':
/* @noinspection PhpMissingBreakStatementInspection */
case 'preview':
switch ($this->request->query->get('type', 'html')) {
case 'css':
break;
case 'content':
$contentPath = $this->request->query->get('contentPath');
if (empty($contentPath)) {
return AccessResult::forbidden('Missing content path.');
}
break;
default:
$entityType = $this->request->query->get('entityType');
$entityId = (int) $this->request->query->get('entityId');
if (empty($entityType) || empty($entityId)) {
return AccessResult::forbidden('Missing entity definition.');
}
}
// Intentionally missing break statement.
case 'schedule':
case 'diagram':
case 'screenshot':
case 'log':
if (empty($this->schedule)) {
return AccessResult::forbidden('Device has no schedule.');
}
if (isset($entityType, $entityId)) {
foreach ($this->schedule->getItems() as $item) {
if ($item['entity']['type'] === $entityType && $item['entity']['id'] === $entityId) {
return AccessResult::allowed();
}
}
foreach ($this->emergency->all() as $item) {
if ($item['entity']['type'] === $entityType && $item['entity']['id'] === $entityId) {
return AccessResult::allowed();
}
}
return AccessResult::forbidden('Requested entity is not published on this device.');
}
return AccessResult::allowed();
default:
return AccessResult::forbidden('Missing or forbidden mode.');
}
}
/**
* Handles the request.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response.
*/
public function request(): Response {
switch ($this->request->query->get('mode')) {
case 'diagram':
$response = $this->diagram();
break;
case 'preview':
if ($this->request->query->get('type', 'html') === 'html') {
$response = $this->preview();
}
else {
$response = $this->previewBinary();
}
break;
case 'load':
$response = match ($this->request->query->get('type', 'html')) {
'html' => $this->load(),
'css' => $this->loadCss(),
'content' => $this->loadContent(),
default => $this->loadBinary(),
};
break;
case 'schedule':
$response = $this->getSchedule();
break;
case 'screenshot':
$response = $this->screenshot();
break;
case 'log':
try {
$response = $this->log();
}
catch (\JsonException) {
$response = '';
}
break;
default:
// This will never happen as we checked this in the access callback.
$response = new AjaxResponse();
}
$event = new Rendered($response, $this->device);
$this->eventDispatcher->dispatch($event, DigitalSignageFrameworkEvents::RENDERED);
return $event->getResponse();
}
/**
* Returns the device screenshot.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The screenshot.
*/
public function screenshot(): AjaxResponse {
if ($screenshot = $this->device->getPlugin()->getScreenshot($this->device, (bool) $this->request->query->get('refresh'))) {
$content = '<img alt="screenshot" src="' . $screenshot['uri'] . '" /><div class="screenshot-widget">' . $screenshot['takenAt'] . '</div>';
}
else {
$content = '<p>' . $this->t('No screenshot available.') . '</p>';
}
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#digital-signage-preview .popup > .content-wrapper > .content', $content));
return $response;
}
/**
* Returns the device logs.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The response.
*
* @throws \JsonException
*/
public function log(): AjaxResponse {
if ($this->request->query->get('type', 'debug') === 'error') {
$logs = $this->device->getPlugin()->showErrorLog($this->device);
}
else {
$logs = $this->device->getPlugin()->showDebugLog($this->device);
}
$rows = [];
foreach ($logs as $item) {
$rows[] = [
'time' => $item['time'],
'payload' => is_scalar($item['payload']) ? $item['payload'] : json_encode($item['payload'], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT),
];
}
$log = [
'#type' => 'table',
'#header' => [
'time' => $this->t('Time'),
'payload' => $this->t('Message'),
],
'#rows' => $rows,
'#empty' => $this->t('No log messages available.'),
'#prefix' => '<div class="table-wrapper pre">',
'#suffix' => '</div>',
];
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#digital-signage-preview .popup > .content-wrapper > .content', $log));
return $response;
}
/**
* Prepare items for rendering.
*
* @param array $libraries
* The libraries.
* @param array $settings
* The settings.
* @param array $items
* The items.
*
* @return array
* List of prepared items.
*/
private function prepareItems(array &$libraries, array &$settings, array $items): array {
$content = [];
foreach ($items as $key => $item) {
if ($item['type'] === 'html') {
$uid = $item['entity']['type'] . '_' . $item['entity']['id'];
if (!isset($content[$uid])) {
$elements = [
'#theme' => 'digital_signage_framework',
'#content' => $this->dsRenderer->buildEntityView(
$item['entity']['type'],
$item['entity']['id'],
$this->device
),
'#full_html' => FALSE,
];
$content[$uid] = $this->dsRenderer->renderPlain($elements);
$htmlHead = $this->prepareHtmlHead($elements);
if (!empty($htmlHead)) {
$content[$uid] .= $this->renderer->renderPlain($htmlHead);
}
if (!empty($elements['#attached']['library'])) {
foreach ($elements['#attached']['library'] as $library) {
$libraries[] = $library;
}
}
if (!empty($elements['#attached']['drupalSettings'])) {
foreach ($elements['#attached']['drupalSettings'] as $settingKey => $value) {
$settings[$settingKey] = $value;
}
}
}
$items[$key]['content'] = $content[$uid];
}
}
return $items;
}
/**
* Get the device schedule.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The schedule.
*/
private function getSchedule(): JsonResponse {
$underlays = new Underlays($this->device);
$this->eventDispatcher->dispatch($underlays, DigitalSignageFrameworkEvents::UNDERLAYS);
$overlays = new Overlays($this->device);
$this->eventDispatcher->dispatch($overlays, DigitalSignageFrameworkEvents::OVERLAYS);
$event = new Libraries($this->device);
$this->eventDispatcher->dispatch($event, DigitalSignageFrameworkEvents::LIBRARIES);
$libraries = $event->getLibraries();
$libraries[] = 'digital_signage_framework/schedule.content';
$libraries[] = 'digital_signage_framework/schedule.timer';
foreach ($overlays->getLibraries() as $library) {
$libraries[] = $library;
}
foreach ($underlays->getLibraries() as $library) {
$libraries[] = $library;
}
$settings = $event->getSettings();
$response = new JsonResponse();
$data = [
'schedule' => $this->prepareItems($libraries, $settings, $this->schedule->getItems()),
'underlays' => $underlays->getUnderlays(),
'overlays' => $overlays->getOverlays(),
'emergencyentities' => $this->prepareItems($libraries, $settings, $this->emergency->all()),
];
$data['assets'] = $this->renderAssets($libraries, $settings, FALSE);
$response->setData($data);
return $response;
}
/**
* Gets an entity render array.
*
* @return array
* The render array.
*/
private function buildEntityView(): array {
return $this->dsRenderer->buildEntityView(
$this->request->query->get('entityType'),
$this->request->query->get('entityId'),
$this->device
);
}
/**
* Load and render the output.
*
* @return \Drupal\Core\Render\AttachmentsInterface
* The html response.
*/
private function load(): AttachmentsInterface {
$output = [
'#theme' => 'digital_signage_framework',
'#content' => $this->buildEntityView(),
];
return $this->dsRenderer->buildHtmlResponse($output);
}
/**
* Get the file URI.
*
* @return string
* The file URI.
*/
private function getFileUri(): string {
/** @var \Drupal\media\MediaInterface|null $media */
$media = $media = Media::load($this->request->query->get('entityId'));
if ($media && $file = File::load($media->getSource()->getSourceFieldValue($media))) {
/** @var \Drupal\file\FileInterface|null $file */
$file_uri = $file->getFileUri();
try {
if (($media->bundle() === 'image') && $image_style = $this->entityTypeManager->getStorage('image_style')->load('digital_signage_' . $this->device->getOrientation())) {
/** @var \Drupal\image\ImageStyleInterface|null $image_style */
$derivative_uri = $image_style->buildUri($file_uri);
if (file_exists($derivative_uri) || $image_style->createDerivative($file_uri, $derivative_uri)) {
$file_uri = $derivative_uri;
}
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
// Deliberately ignored.
}
return $file_uri;
}
return DRUPAL_ROOT . theme_get_setting('logo.url', $this->theme);
}
/**
* Prepares the html header.
*
* @param array $elements
* The render elements.
*
* @return array
* The html header.
*/
private function prepareHtmlHead(array $elements): array {
if (!isset($elements['#attached']['html_head'])) {
return [];
}
$result = [];
foreach ($elements['#attached']['html_head'] as $item) {
if (is_array($item)) {
foreach ($item as $value) {
if (is_array($value) && isset($value['#tag']) && $value['#tag'] === 'style') {
$value['#type'] = 'html_tag';
$result[] = $value;
}
}
}
}
return $result;
}
/**
* Renders all the assets.
*
* @param array $libraries
* The libraries.
* @param array $settings
* The settings.
* @param bool $inlineCSS
* The inline CSS assets.
* @param array $htmlHead
* The html header.
*
* @return string
* The rendered assets.
*/
private function renderAssets(array $libraries, array $settings, bool $inlineCSS, array $htmlHead = []): string {
$assets = new AttachedAssets();
$assets->setLibraries($libraries);
$assets->setSettings($settings);
[$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, FALSE);
$css_assets = $this->assetResolver->getCssAssets($assets, FALSE);
if ($inlineCSS) {
$fonts = Yaml::decode($this->config->get('fonts')) ?? [];
$fontCSS = '';
foreach ($fonts as $font) {
if ($font['enabled']) {
$fontCSS .= '@font-face {font-family: "' . $font['family'] . '";';
$fontCSS .= 'font-weight:' . $font['weight'] . ';';
$fontCSS .= 'font-style:' . $font['style'] . ';';
$fontCSS .= 'src:';
$firstFont = TRUE;
foreach ($font['formats'] as $format => $url) {
if (!$firstFont) {
$fontCSS .= ',';
}
else {
$firstFont = FALSE;
}
$fontCSS .= 'url("' . $url . '") format("' . $format . '")';
}
$fontCSS .= ';}';
}
}
$css = '<style>' . $fontCSS . $this->prepareCss($css_assets) . '</style>';
}
else {
$cssAsset = $this->cssAssetCollectionRenderer->render($css_assets);
$css = $this->renderer->renderPlain($cssAsset);
}
$assets_header = $this->jsAssetCollectionRenderer->render($js_assets_header);
$assets_footer = $this->jsAssetCollectionRenderer->render($js_assets_footer);
foreach ($htmlHead as $item) {
$assets_header[] = $item;
}
return $css . $this->renderer->renderPlain($assets_header) . $this->renderer->renderPlain($assets_footer);
}
/**
* Prepares and renders CSS and JS assets.
*
* @param array $build
* The render array.
*
* @return string
* The prepared and rendered assets.
*/
private function prepareCssJs(array $build): string {
$event = new Libraries($this->device);
$event->addLibrary('digital_signage_framework/schedule.preview-iframe');
$event->addLibrary('digital_signage_framework/schedule.timer');
if (!empty($build['#attached']['library'])) {
foreach ($build['#attached']['library'] as $library) {
$event->addLibrary($library);
}
}
if (!empty($build['#attached']['drupalSettings'])) {
foreach ($build['#attached']['drupalSettings'] as $key => $value) {
$event->addSettings($key, (array) $value);
}
}
$this->eventDispatcher->dispatch($event, DigitalSignageFrameworkEvents::LIBRARIES);
return $this->renderAssets($event->getLibraries(), $event->getSettings(), TRUE, $this->prepareHtmlHead($build));
}
/**
* Prepares and renders CSS files.
*
* @param array $files
* The list of CSS files.
*
* @return string
* The prepared and rendered CSS files.
*/
private function prepareCss(array $files = []): string {
$cssFiles = [];
foreach ($files as $file) {
$cssFiles[] = $file['data'];
}
if ($this->moduleHandler->moduleExists('layout_builder')) {
foreach ($this->libraryDiscovery->getLibrariesByExtension('layout_builder') as $library) {
foreach ($library['css'] as $css) {
$cssFiles[] = $css['data'];
}
}
}
$cssFiles[] = $this->moduleExtensionList->getPath('digital_signage_framework') . '/css/digital-signage.css';
$cssFiles[] = $this->moduleExtensionList->getPath('digital_signage_framework') . '/css/overlays.css';
foreach (explode(PHP_EOL, str_replace("\r", '', $this->config->get('css'))) as $file) {
$cssFiles[] = $file;
}
$css = '';
foreach ($cssFiles as $cssFile) {
if (!empty($cssFile) && file_exists($cssFile)) {
$css .= file_get_contents($cssFile) . PHP_EOL;
}
}
return $css;
}
/**
* Load all CSS components.
*
* @return \Symfony\Component\HttpFoundation\Response
* The CSS components.
*/
private function loadCss(): Response {
$css = $this->prepareCss();
$headers = [
'Content-Type' => 'text/css',
];
return new Response($css, 200, $headers);
}
/**
* Load extra content.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* The content.
*/
private function loadContent(): BinaryFileResponse {
$content_path = trim(Url::fromRoute('<front>', [], [
'absolute' => TRUE,
'path_processing' => FALSE,
'language' => FALSE,
])->toString(), '/') . base64_decode($this->request->query->get('contentPath'));
$file_uri = 'temporary://content-' . hash('md5', $content_path);
try {
$client = $this->clientFactory->fromOptions(['base_uri' => $content_path]);
$options = [];
$authorization = $this->request->headers->get('authorization');
if ($authorization) {
$options[RequestOptions::HEADERS]['Authorization'] = $authorization;
}
$response = $client->request('get', $content_path, $options);
$content = $response->getBody()->getContents();
if ($this->config->get('hotfix_svg') && str_starts_with($content, '<svg')) {
$content = '<?xml version="1.0" standalone="no"?>' . $content;
}
file_put_contents($file_uri, $content);
}
catch (GuzzleException) {
file_put_contents($file_uri, '');
}
$headers = [
'Content-Type' => mime_content_type($file_uri),
];
return new BinaryFileResponse($file_uri, 200, $headers);
}
/**
* Loads a binary response.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* The binary response.
*/
private function loadBinary(): BinaryFileResponse {
$file_uri = $this->getFileUri();
$headers = [
'Content-Type' => mime_content_type($file_uri),
];
return new BinaryFileResponse($file_uri, 200, $headers);
}
/**
* Prepares output for an ajax response to display in a popup.
*
* @param array|string $output
* The output.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The ajax response.
*/
private function popupContent(array|string $output): AjaxResponse {
$underlays = new Underlays($this->device);
$this->eventDispatcher->dispatch($underlays, DigitalSignageFrameworkEvents::UNDERLAYS);
$overlays = new Overlays($this->device);
$this->eventDispatcher->dispatch($overlays, DigitalSignageFrameworkEvents::OVERLAYS);
$content = is_array($output) ? $this->renderer->renderRoot($output) : $output;
$build = is_array($output) ? $output : [];
foreach ($overlays->getLibraries() as $library) {
$build['#attached']['library'][] = $library;
}
foreach ($underlays->getLibraries() as $library) {
$build['#attached']['library'][] = $library;
}
$origin = Url::fromUserInput('/', [
'absolute' => TRUE,
'language' => $this->languageManager->getLanguage(LanguageInterface::LANGCODE_NOT_SPECIFIED),
])->toString();
$build['#attached']['drupalSettings']['digital_signage_preview_iframe'] = [
'origin' => $origin,
];
$cssjs = $this->prepareCssJs($build);
$content = '<div id="underlays">' . implode('', $underlays->getUnderlays()) . '</div>' . $content . '<div id="overlays">' . implode('', $overlays->getOverlays()) . '</div>';
$content = '<iframe srcdoc="' . htmlspecialchars($cssjs . $content) . '" src="' . $origin . '" width="' . $this->device->getWidth() . '" height="' . $this->device->getHeight() . '"></iframe>';
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#digital-signage-preview .popup > .content-wrapper > .content', $content));
return $response;
}
/**
* Returns the preview response.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The preview response.
*/
private function preview(): AjaxResponse {
return $this->popupContent($this->buildEntityView());
}
/**
* Returns the binary for preview.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The binary for preview.
*/
private function previewBinary(): AjaxResponse {
$file_uri = $this->fileUrlGenerator->generateAbsoluteString($this->getFileUri());
$output = match ($this->request->query->get('type')) {
'image' => '<img src="' . $file_uri . '" alt="" />',
'video' => '<video src="' . $file_uri . '" autoplay="autoplay"></video>',
default => 'Problem loading media',
};
return $this->popupContent($output);
}
/**
* Returns the diagram for preview.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The diagram for preview.
*/
private function diagram(): AjaxResponse {
$items = $this->schedule->getItems();
$uml = match ($this->request->query->get('umlType')) {
'activity' => $this->umlActivity($items),
default => $this->umlSequence($items),
};
try {
$uri = $this->config->get('plantuml_url') . '/svg/' . encodep($uml);
$client = $this->clientFactory->fromOptions(['base_uri' => $uri]);
$response = $client->request('get', $uri);
$output = '<div class="uml-diagram">' . $response->getBody()->getContents() . '</div>';
}
catch (GuzzleException) {
$output = '';
}
catch (\Exception) {
$output = 'Problem encoding UML: <br><pre>' . $uml . '</pre>';
}
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#digital-signage-preview .popup > .content-wrapper > .content', $output));
return $response;
}
/**
* Gets the entity label.
*
* @param string $type
* The entity type.
* @param int $id
* The entity ID.
*
* @return string
* The label.
*/
private function getEntityLabel(string $type, int $id): string {
try {
/** @var \Drupal\Core\Entity\ContentEntityInterface|null $entity */
$entity = $this->entityTypeManager->getStorage($type)->load($id);
if (($entity !== NULL) && $label = $entity->label()) {
return $label;
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
// Can be ignored.
}
return 'N/A';
}
/**
* Render the items as activity UML diagram.
*
* @param array $items
* The items.
*
* @return string
* The UML diagram.
*/
private function umlActivity(array $items): string {
$uml = '@startuml' . PHP_EOL . 'while (Loop)' . PHP_EOL;
foreach ($items as $item) {
$uml .= strtr(':**%label**%EOL%type: **%entityType/%entityId**%EOL%duration seconds%dynamic;%EOL', [
'%EOL' => PHP_EOL,
'%label' => $this->getEntityLabel($item['entity']['type'], $item['entity']['id']),
'%type' => $item['type'],
'%entityType' => $item['entity']['type'],
'%entityId' => $item['entity']['id'],
'%duration' => $item['duration'],
'%dynamic' => $item['dynamic'] ? '/dynamic' : '',
]);
}
$uml .= 'endwhile' . PHP_EOL . 'stop' . PHP_EOL . '@enduml';
return $uml;
}
/**
* Render the items as sequence UML diagram.
*
* @param array $items
* The items.
*
* @return string
* The UML diagram.
*/
private function umlSequence(array $items): string {
$uml = '@startuml' . PHP_EOL;
foreach ($items as $item) {
$uml .= strtr('participant "%label" as %id << %type: %path >>%EOL', [
'%EOL' => PHP_EOL,
'%label' => $this->getEntityLabel($item['entity']['type'], $item['entity']['id']),
'%type' => $item['type'],
'%path' => $item['entity']['type'] . '/' . $item['entity']['id'],
'%id' => $item['entity']['type'] . '_' . $item['entity']['id'],
]);
}
$previous = FALSE;
foreach ($items as $item) {
$current = $item['entity']['type'] . '_' . $item['entity']['id'];
if ($previous) {
$uml .= strtr('%previous --> %id: %duration seconds%EOL', [
'%EOL' => PHP_EOL,
'%previous' => $previous,
'%id' => $current,
'%duration' => $item['duration'],
]);
}
$previous = $current;
}
$uml .= '@enduml';
return $uml;
}
}
