Commit 62785cc0 authored by Stephen Lucero's avatar Stephen Lucero
Browse files

Issue #3293860 by slucero: Rendering Invalid Pattern Data Causes a White Screen of Death

parent 50c7f8af
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -22,7 +22,8 @@
    },
    "require-dev": {
        "drupal/core-recommended": "^9",
        "drupal/core-dev": "^9"
        "drupal/core-dev": "^9",
        "drupal/devel": "^4.1"
    },
    "extra": {
        "drush": {
+5 −1
Original line number Diff line number Diff line
@@ -54,6 +54,10 @@ services:
      - '@patternkit.asset.library'
      - '@patternkit.schema.schema_walker_factory'
      - '@logger.channel.patternkit'
  patternkit.schema.schema_factory:
    class: Drupal\patternkit\Schema\SchemaFactory
    arguments:
      - '@patternkit.schema.ref_provider'
  patternkit.schema.ref_provider:
    class: Drupal\patternkit\Schema\PatternkitRefProvider
    arguments:
@@ -62,5 +66,5 @@ services:
  patternkit.schema.schema_walker_factory:
    class: Drupal\patternkit\Schema\SchemaWalkerFactory
    arguments:
      - '@patternkit.schema.ref_provider'
      - '@patternkit.schema.schema_factory'
    public: false
+83 −37
Original line number Diff line number Diff line
@@ -2,10 +2,15 @@

namespace Drupal\patternkit\Element;

use Drupal\patternkit\PatternFieldProcessorPluginManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\patternkit\Entity\PatternInterface;
use Drupal\patternkit\Exception\SchemaException;
use Drupal\patternkit\PatternLibraryPluginInterface;
use Drupal\patternkit\PatternLibraryPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a render element to display a pattern.
@@ -19,7 +24,7 @@ use Drupal\patternkit\PatternLibraryPluginInterface;
 * @code
 * $build['example_pattern'] = [
 *   '#type' => 'pattern',
 *   '#pattern' => 'example',
 *   '#pattern' => Pattern,
 *   '#config' => [
 *     'text' => '[node:title]',
 *     'formatted_text' => '<p><strong>My formatted text</strong></p>',
@@ -32,7 +37,21 @@ use Drupal\patternkit\PatternLibraryPluginInterface;
 *
 * @RenderElement("pattern")
 */
class Pattern extends RenderElement {
class Pattern extends RenderElement implements ContainerFactoryPluginInterface {

  /**
   * The pattern field processor plugin manager.
   *
   * @var \Drupal\patternkit\PatternFieldProcessorPluginManager
   */
  protected PatternFieldProcessorPluginManager $fieldProcessorPluginManager;

  /**
   * The pattern library plugin manager.
   *
   * @var \Drupal\patternkit\PatternLibraryPluginManager
   */
  protected PatternLibraryPluginManager $libraryPluginManager;

  /**
   * {@inheritdoc}
@@ -40,7 +59,7 @@ class Pattern extends RenderElement {
  public function getInfo(): array {
    return [
      '#pre_render' => [
        [get_class($this), 'preRenderPatternElement'],
        [$this, 'preRenderPatternElement'],
      ],
      '#pattern' => NULL,
      '#config' => [],
@@ -48,6 +67,40 @@ class Pattern extends RenderElement {
    ];
  }

  /**
   * Constructs a \Drupal\Component\Plugin\PluginBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\patternkit\PatternFieldProcessorPluginManager $fieldProcessorPluginManager
   *   The field processor plugin manager service.
   * @param \Drupal\patternkit\PatternLibraryPluginManager $libraryPluginManager
   *   The pattern library parser plugin manager service.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, PatternFieldProcessorPluginManager $fieldProcessorPluginManager, PatternLibraryPluginManager $libraryPluginManager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->fieldProcessorPluginManager = $fieldProcessorPluginManager;
    $this->libraryPluginManager = $libraryPluginManager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('plugin.manager.pattern_field_processor'),
      $container->get('plugin.manager.library.pattern'),
    );
  }

  /**
   * Pattern element pre render callback.
   *
@@ -59,29 +112,43 @@ class Pattern extends RenderElement {
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  public static function preRenderPatternElement(array $element): array {
  public function preRenderPatternElement(array $element): array {
    /** @var \Drupal\patternkit\Entity\Pattern $pattern */
    $pattern = $element['#pattern'];

    // Fail early if a pattern was unable to be loaded.
    if (is_null($pattern)) {
      $element = [
      return [
        '#markup' => t('Pattern unavailable.'),
      ];
      return $element;
    }

    $pattern->config = $element['#config'];
    $pattern->context = $element['#context'];

    try {
      $bubbleableMetadata = new BubbleableMetadata();
    static::preprocessConfigValues($pattern, $pattern->config, $pattern->context, $bubbleableMetadata);
      $this->fieldProcessorPluginManager->processSchemaValues($pattern, $pattern->config, $pattern->context, $bubbleableMetadata);

    $library_plugin = static::getPatternLibraryPlugin($pattern);
      $library_plugin = $this->getPatternLibraryPlugin($pattern);
      $elements = $library_plugin->render([$pattern]);

      // Apply all bubbleable metadata from preprocessing.
      $bubbleableMetadata->applyTo($elements);
    }
    catch (SchemaException $exception) {
      // Replace the pattern output with an error element for more sophisticated
      // output handling.
      // @see \Drupal\patternkit\Element\PatternError
      $elements = [];
      $elements['error'] = [
        '#type' => 'pattern_error',
        '#pattern' => $element['#pattern'],
        '#config' => $element['#config'],
        '#context' => $element['#context'],
        '#exception' => $exception,
      ];
    }

    return $elements;
  }
@@ -97,34 +164,13 @@ class Pattern extends RenderElement {
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  protected static function getPatternLibraryPlugin(PatternInterface $pattern): PatternLibraryPluginInterface {
    /** @var \Drupal\patternkit\PatternLibraryPluginManager $pattern_plugin_manager */
    $patternLibraryPluginManager = \Drupal::service('plugin.manager.library.pattern');

  protected function getPatternLibraryPlugin(PatternInterface $pattern): PatternLibraryPluginInterface {
    $pattern_plugin = $pattern->getLibraryPluginId();
    $library_plugin_id = !empty($pattern_plugin) ? $pattern_plugin : 'twig';

    return $patternLibraryPluginManager->createInstance($library_plugin_id);
  }

  /**
   * Execute value processing on configuration values.
   *
   * @param \Drupal\patternkit\Entity\PatternInterface $pattern
   *   The pattern being prepared and processed.
   * @param array $config
   *   Configuration values for the pattern being prepared.
   * @param array $context
   *   Context values configured for the pattern being prepared.
   * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
   *   Bubbleable metadata to be tracked and updated during processing.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  public static function preprocessConfigValues(PatternInterface $pattern, array &$config, array $context, BubbleableMetadata $bubbleable_metadata): void {
    /** @var \Drupal\patternkit\PatternFieldProcessorPluginManager $manager */
    $manager = \Drupal::service('plugin.manager.pattern_field_processor');
    $manager->processSchemaValues($pattern, $config, $context, $bubbleable_metadata);
    /** @var \Drupal\patternkit\PatternLibraryPluginInterface */
    $plugin = $this->libraryPluginManager->createInstance($library_plugin_id);
    return $plugin;
  }

}
+134 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\patternkit\Element;

use Drupal\Core\Render\Element\RenderElement;

/**
 * Provides a render element to display in place of a pattern error.
 *
 * If a pattern fails to load or render where expected, this render element
 * may be used as a replacement to handle a graceful degradation experience
 * of what should be displayed in place of the failed pattern content.
 *
 * Properties:
 * - '#pattern': The loaded Pattern entity to be rendered.
 * - '#config': Configuration to be passed to the pattern for rendering.
 * - '#context': Context values for rendering the pattern.
 * - '#exception': The exception object causing the render failure.
 *
 * Usage Example:
 * @code
 * $build['failed_pattern'] = [
 *   '#type' => 'pattern_error',
 *   '#pattern' => Pattern,
 *   '#config' => [
 *     'text' => '[node:title]',
 *     'formatted_text' => '<p><strong>My formatted text</strong></p>',
 *   ],
 *   '#context' => [
 *     'node' => $node,
 *   ],
 *   '#exception' => Exception,
 * ];
 * @endcode
 *
 * @RenderElement("pattern_error")
 */
class PatternError extends RenderElement {

  /**
   * The permission to check for including debug output in error displays.
   */
  const DEBUG_PERMISSION = 'access devel information';

  /**
   * {@inheritdoc}
   */
  public function getInfo(): array {
    return [
      '#pre_render' => [
        [$this, 'preRenderPatternErrorElement'],
        [$this, 'preRenderDebugOutput'],
      ],
      '#pattern' => NULL,
      '#config' => [],
      '#context' => [],
      '#exception' => NULL,
    ];
  }

  /**
   * Pattern error element pre render callback to handle basic failure display.
   *
   * @param array $element
   *   An associative array containing the properties of the pattern element.
   *
   * @return array
   *   The modified element.
   */
  public function preRenderPatternErrorElement(array $element): array {
    /** @var \Drupal\patternkit\Entity\Pattern $pattern */
    $pattern = $element['#pattern'];

    // Return an error message to display in place of the rendered pattern.
    $message = $this->t('Failed to render pattern %pattern (%pattern_id).', [
      '%pattern' => $pattern->getName(),
      '%pattern_id' => $pattern->getAssetId(),
    ]);
    $element['message'] = [
      '#markup' => $message,
    ];

    return $element;
  }

  /**
   * Secondary pre render callback to prepare debug information if applicable.
   *
   * @param array $element
   *   An associative array containing the properties of the pattern element.
   *
   * @return array
   *   The modified element.
   */
  public function preRenderDebugOutput(array $element): array {
    // Skip altogether if the user doesn't have access to dev output.
    if (!$this->shouldDisplayDebugOutput()) {
      return $element;
    }

    if (isset($element['#exception'])) {
      /** @var \Exception $exception */
      $exception = $element['#exception'];

      // Collect all debug information in a collapsed container to avoid
      // overwhelming the user, especially in the case of multiple failures on
      // a single page.
      $element['debug'] = [
        '#type' => 'details',
        '#title' => $this->t('Debug output'),
        '#open' => FALSE,
      ];

      // Expose the exception message in a formatted block for easier
      // parsing by developers.
      $element['debug']['message'] = [
        '#markup' => '<pre>' . $exception->getMessage() . '</pre>',
      ];
    }

    return $element;
  }

  /**
   * Test if debug output should be displayed.
   *
   * @return bool
   *   TRUE if debug output should be displayed. FALSE otherwise.
   */
  protected function shouldDisplayDebugOutput(): bool {
    return \Drupal::currentUser()->hasPermission(static::DEBUG_PERMISSION);
  }

}
+1 −1
Original line number Diff line number Diff line
@@ -3,6 +3,6 @@
namespace Drupal\patternkit\Exception;

/**
 * Base exception for unexpected events during Schema processing.
 * An exception that occurs from errors processing references in schemas.
 */
class SchemaReferenceException extends SchemaException {}
Loading