component_library-1.0.x-dev/src/OverrideMode.php

src/OverrideMode.php
<?php

declare(strict_types=1);

namespace Drupal\component_library;

use Drupal\Component\Serialization\Json;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\component_library\Entity\ComponentOverride;
use Drupal\component_library\Event\AddVariablesPlaceholderEvent;
use Drupal\component_library\Event\OverrideIgnoreTemplateEvent;
use Drupal\component_library\Event\ReplaceVariablesPlaceholderEvent;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Theme\Registry;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Twig\Loader\LoaderInterface;

final class OverrideMode {

  private Registry $registry;
  private UuidInterface $uuid;
  private ImmutableConfig $config;
  private LoaderInterface $twigLoader;
  private RendererInterface $renderer;
  private AccountProxyInterface $currentUser;
  private ThemeManagerInterface $themeManager;
  private EventDispatcherInterface $dispatcher;
  private LoggerChannelFactoryInterface $logger;
  private EntityStorageInterface $overrideStorage;
  private RedirectDestinationInterface $destination;

  public function __construct(ThemeManagerInterface $theme_manager, Registry $registry, RedirectDestinationInterface $destination, LoggerChannelFactoryInterface $logger, RendererInterface $renderer, EventDispatcherInterface $dispatcher, UuidInterface $uuid, LoaderInterface $twig_loader, ConfigFactoryInterface $config, AccountProxyInterface $current_user, EntityTypeManagerInterface $entity_type_manager) {
    $this->uuid = $uuid;
    $this->logger = $logger;
    $this->registry = $registry;
    $this->renderer = $renderer;
    $this->dispatcher = $dispatcher;
    $this->twigLoader = $twig_loader;
    $this->destination = $destination;
    $this->currentUser = $current_user;
    $this->themeManager = $theme_manager;
    $this->config = $config->get('component_library.override_mode.settings');
    $this->overrideStorage = $entity_type_manager->getStorage('component_override');
  }

  /**
   * Attach override libraries.
   *
   * @param string $template_file
   *   The template to override.
   * @param array $variables
   *   The template variables.
   *
   * @throws \Exception
   */
  public function attachLibraries(string $template_file, array $variables): void {
    $override = $this->twigLoader->getOverrideByName($template_file);
    if ($override instanceof ComponentOverride) {
      $libraries = $override->get('libraries');
      if ($libraries) {
        $bubbleable = [];
        foreach ($libraries as $library) {
          $bubbleable['#attached']['library'][] = $library;
        }
        $this->renderer->render($bubbleable);
      }
    }
  }

  /**
   * Prepare override details.
   *
   * Sets up the URL and settings that override_mode.js needs to prepopulate
   * the ComponentOverrideForm.
   *
   * @param string $template_file
   *   The template to override.
   * @param array $variables
   *   The template variables.
   *
   * @return string|null
   *   The uuid used as a front-end selector or NULL.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function prepareOverrideDetails(string $template_file, array $variables): ?string {
    if (!$this->config->get('override_mode')) {
      return NULL;
    }
    if (!$this->currentUser->hasPermission('access override mode')) {
      return NULL;
    }
    $ignore = $this->dispatcher->dispatch(new OverrideIgnoreTemplateEvent($template_file, $variables))->isIgnored();
    if ($ignore) {
      return NULL;
    }
    $size = $this->examineArray($variables);
    if ($size > $this->config->get('variables_max_size')) {
      $this->logger->get('component_library')->warning('Size (%size) exceeds variables max size for (%template).', [
        '%size' => $size,
        '%template' => $template_file,
      ]);
      return NULL;
    }
    $memory_usage = \round(\memory_get_usage() / 1_048_576);
    if ($memory_usage > $this->config->get('memory_limit')) {
      $this->logger->get('component_library')->warning('Usage (%mem_usage) exceeds max memory limit for %template.', [
        '%mem_usage' => $memory_usage,
        '%template' => $template_file,
      ]);
      return NULL;
    }
    $uuid = $this->getUuid();
    $base_hook = $variables['theme_hook_original'];
    $suggestions = $variables['theme_hook_suggestions'];
    $cache_tag = "override:$base_hook";
    \array_unshift($suggestions, $base_hook);
    $theme = $this->themeManager->getActiveTheme()->getName();

    // Get the current theme registry.
    $theme_suggestion = '';
    $theme_registry = $this->registry->getRuntime();
    foreach (\array_reverse($suggestions) as $theme_suggestion) {
      if ($theme_registry->has($theme_suggestion)) {
        break;
      }
    }
    if ($theme_suggestion === '') {
      return NULL;
    }
    $overrides = $this->overrideStorage->loadByProperties([
      'override' => $theme_suggestion,
      'theme' => $theme,
    ]);

    // Create a URL to add or update an existing override.
    $options = [
      'query' => [
        'uuid' => $uuid,
        'override_options' => $suggestions,
        'theme_suggestion' => $theme_suggestion,
        'destination' => $this->destination->get(),
      ],
    ];
    if (empty($overrides)) {
      $route = 'entity.component_override.add_form';
      $route_parameters = [
        'theme' => $theme,
      ];
      $options['query'] += [
        'base_hook' => $base_hook,
        'cache' => $cache_tag,
        'plugin_value' => 'override_mode',
      ];
    }
    else {
      $route = 'entity.component_override.edit_form';
      $route_parameters = [
        'component_override' => \array_key_first($overrides),
      ];
    }

    $bubbleable = [];
    $bubbleable['#cache']['tags'][] = $cache_tag;
    // Pass details to override this template to JS.
    $bubbleable['#attached']['drupalSettings']['overrides'][$uuid] = [
      'uuid' => $uuid,
      'url' => Url::fromRoute($route, $route_parameters, $options)->toString(),
      'theme_suggestion' => $theme_suggestion,
    ];
    // This needs to be refactored as it now throws
    // Deprecated function: Creation of dynamic property Drupal\Core\Template\Attribute::$#override_mode_breadcrumb is deprecated in Drupal\component_library\OverrideMode->traverse()
    // $variables = $this->addPlaceholders($variables);
//    try {
//      $bubbleable['#attached']['html_head'][] = [
//        [
//          '#type' => 'html_tag',
//          '#tag' => 'script',
//          '#weight' => -1,
//          '#attributes' => [
//            'type' => 'application/json',
//            'data-uuid' => $uuid,
//          ],
//          '#value' => Json::encode([]),
//        ],
//        $uuid,
//      ];
//    }
//    catch (\Throwable $t) {
//      $this->logger->get('component_library')->notice("component_library Override Mode: failed to serialize $theme_suggestion variables.\n" . $t->getMessage());
//    }
    $this->renderer->render($bubbleable);

    return $uuid;
  }

  /**
   * Add placeholders.
   *
   * Variables are used when previewing template changes. They are serialized,
   * passed to JS, and then submitted to the ComponentOverrideForm form in an
   * AJAX POST for previews in Override Mode before being saved to an Component
   * Override for previewing again at a later date. Let's drastically cut down
   * on the size of the variables by adding placeholders for verbose items like
   * entities, views, etc.
   *
   * @param array $variables
   *   The template variables.
   *
   * @return array
   *   The variables with placeholders.
   */
  private function addPlaceholders(array $variables): array {
    return $this->traverse($variables, [$this, 'askForPlaceholder']);
  }

  /**
   * Replace placeholders.
   *
   * @param array $variables
   *   The template variables.
   *
   * @return array
   *   The variables with replacements.
   */
  public function replacePlaceholders(array $variables): array {
    return $this->traverse($variables, [$this, 'askForReplacement']);
  }

  /**
   * Ask for placeholder.
   *
   * @param $value
   *   The item to be replaced.
   * @param \Iterator $iterator
   *   The recursive iterator.
   * @param array $variables
   *   The template variables.
   *
   * @return mixed|null
   *   The placeholder or NULL.
   */
  private function askForPlaceholder(&$value, \Iterator $iterator, array $variables) {
    if (\is_object($value) || (\is_array($value) && isset($value['#type']))) {
      $parents = [];
      for ($i = 0; $i <= $iterator->getDepth(); $i++) {
        $parents[] = $iterator->getSubIterator($i)->key();
      }
      $event = new AddVariablesPlaceholderEvent($value, $parents, $variables);
      $event = $this->dispatcher->dispatch($event);
      return $event->getPlaceholder();
    }
    return NULL;
  }

  /**
   * Ask for replacement.
   *
   * @param $value
   *   The item to be replaced.
   * @param \Iterator$iterator
   *   The recursive iterator.
   * @param array $variables
   *   The template variables.
   *
   * @return mixed|null
   *   The replacement or NULL.
   */
  private function askForReplacement(&$value, \Iterator $iterator, array $variables) {
    if (\is_array($value) && isset($value['#override_mode'])) {
      $event = new ReplaceVariablesPlaceholderEvent($value['#override_mode']);
      $event = $this->dispatcher->dispatch($event);
      return $event->getReplacement();
    }
    return NULL;
  }

  /**
   * Traverse.
   *
   * Recursively iterate over template variables.
   *
   * @param array $variables
   *   The template variables.
   * @param callable $callback
   *   askForPlaceholder or askForReplacement.
   *
   * @return array
   *   The modified template variables.
   */
  private function traverse(array $variables, callable $callback): array {
    $array_iterator = new \RecursiveArrayIterator($variables);
    $filter_iterator = new \RecursiveCallbackFilterIterator($array_iterator, [
      $this, 'isCyclic',
    ]);
    $recursive_iterator = new \RecursiveIteratorIterator($filter_iterator, \RecursiveIteratorIterator::SELF_FIRST);
    $recursive_iterator->setMaxDepth(20);
    foreach ($recursive_iterator as $value) {

      $replacement = $callback($value, $recursive_iterator, $variables);
      if ($replacement) {
        $this->replace($recursive_iterator, $replacement);
        continue;
      }

      // Avoid recursion by marking where we've been.
      if (\is_object($value)) {
        $value->{'#override_mode_breadcrumb'} = TRUE;
      }
      if (\is_array($value)) {
        $value['#override_mode_breadcrumb'] = TRUE;
      }
    }
    return \iterator_to_array($recursive_iterator);
  }

  /**
   * Replace.
   *
   * Adds or replaces a placeholder in the template variables being traversed.
   *
   * @param \RecursiveIteratorIterator $iterator
   *   The iterator.
   * @param mixed $replacement
   *   The item or placeholder to be replaced.
   */
  private function replace(\RecursiveIteratorIterator $iterator, $replacement): void {
    $current_depth = $iterator->getDepth();
    for ($sub_depth = $current_depth; $sub_depth >= 0; $sub_depth--) {
      $subIterator = $iterator->getSubIterator($sub_depth);
      $subIterator->offsetSet($subIterator->key(), ($sub_depth === $current_depth ? $replacement : $iterator->getSubIterator($sub_depth + 1)->getArrayCopy()));
    }
  }

  /**
   * Is cyclic.
   *
   * RecursiveCallbackFilterIterator filter callback to prevent recursion.
   *
   * @param mixed $current
   *   The current item being traversed.
   * @param string $key
   *   The items key.
   * @param \RecursiveArrayIterator $iterator
   *   The iterator.
   *
   * @return bool
   *   Whether or not to traverse this item.
   */
  public function isCyclic($current, string $key, \RecursiveArrayIterator $iterator): bool {
    // Skip closures.
    if ($current instanceof \Closure) {
      return FALSE;
    }
    // Avoid infinite loops by checking if we've been here before.
    // e.g. View > query > view > query ...
    if (\is_array($current) && isset($current['#override_mode_breadcrumb'])) {
      return FALSE;
    }
    if (\is_object($current) && isset($current->{'#override_mode_breadcrumb'})) {
      return FALSE;
    }

    // Some arrays are impossible to detect reference recursion by marking where
    // you have already been.
    //
    // e.g. #groups when set as $groups[$group][] = &$element
    // in RenderElement::processGroup
    //
    // Let's leverage count($current, COUNT_RECURSIVE) to detect infinite
    // reference recursion.
    //
    // @see https://hbgl.dev/detecting-cyclic-arrays-in-php-a-new-approach/
    static $parents;
    if (\is_null($parents)) {
      $parents = [];
    }
    if (\is_array($current)) {
      $is_recursive = $this->examineArray($current, TRUE);
      if ($is_recursive) {
        // This array is recursive, but lets continue adding and replacing
        // placeholders until it starts repeating.
        $parents[] = $key;
        $recursion_depth = 5;
        if (\count($parents) >= 5) {
          $slice = \array_slice($parents, -$recursion_depth, $recursion_depth);
          foreach ($parents as $parents_key => $parent) {
            if ($parent === $slice[0]) {
              $repeating = TRUE;
              foreach ($slice as $value) {
                if ($value === $parents[$parents_key]) {
                  $parents_key++;
                  continue;
                }
                $repeating = FALSE;
              }
              if ($repeating) {
                return FALSE;
              }
            }
          }
        }
      }
    }
    return TRUE;
  }

  /**
   * Examine array.
   *
   * @param array $variables
   *   The array to examine.
   * @param bool $detect_recursion
   *   Whether or not to return the array size or if the array has infinite
   *   recursion.
   *
   * @return bool|int
   *   The size of the array or BOOL if detecting_recursion.
   */
  private function examineArray(array $variables, bool $detect_recursion = FALSE) {
    $is_recursive = FALSE;
    \set_error_handler(static function ($errno, $errstr) use (&$is_recursive): void {
      $is_recursive = $errno === \E_WARNING && \mb_stripos($errstr, 'recursion');
    });
    try {
      $size = \count($variables, \COUNT_RECURSIVE);
    } finally {
      \restore_error_handler();
    }
    if ($detect_recursion) {
      return $is_recursive;
    }
    return $size;
  }

  /**
   * Ensure UUIDs are not duplicated.
   *
   * @return string
   *   A unique UUID.
   */
  private function getUuid(): string {
    $uuids = &drupal_static(__FUNCTION__);
    if (!isset($uuids)) {
      $uuids = [];
    }
    do {
      $uuid = $this->uuid->generate();
    } while (\in_array($uuid, $uuids, TRUE));
    $uuids[] = $uuid;

    return $uuid;
  }

  /**
   * Get twig debug message.
   *
   * @return string
   *   A link to the override.
   */
  public function getTwigDebugMessage($name): string {
    $debug = '';
    $override = $this->twigLoader->getOverrideByName($name);
    if ($override) {
      $debug = 'Overridden by ';
      $debug .= $override->toUrl('edit-form', ['absolute' => TRUE])->toString();
      $debug .= ' ';
    }

    return $debug;
  }

}

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

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