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

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc