Mercurial > hg > isophonics-drupal-site
diff core/lib/Drupal/Core/Template/TwigExtension.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | af1871eacc83 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/lib/Drupal/Core/Template/TwigExtension.php Wed Nov 29 16:09:58 2017 +0000 @@ -0,0 +1,633 @@ +<?php + +namespace Drupal\Core\Template; + +use Drupal\Component\Utility\Html; +use Drupal\Component\Render\MarkupInterface; +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\Render\AttachmentsInterface; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Render\Markup; +use Drupal\Core\Render\RenderableInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Routing\UrlGeneratorInterface; +use Drupal\Core\Theme\ThemeManagerInterface; +use Drupal\Core\Url; + +/** + * A class providing Drupal Twig extensions. + * + * This provides a Twig extension that registers various Drupal-specific + * extensions to Twig, specifically Twig functions, filter, and node visitors. + * + * @see \Drupal\Core\CoreServiceProvider + */ +class TwigExtension extends \Twig_Extension { + + /** + * The URL generator. + * + * @var \Drupal\Core\Routing\UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The theme manager. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface + */ + protected $themeManager; + + /** + * The date formatter. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * Constructs \Drupal\Core\Template\TwigExtension. + * + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator + * The URL generator. + * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager + * The theme manager. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter. + */ + public function __construct(RendererInterface $renderer, UrlGeneratorInterface $url_generator, ThemeManagerInterface $theme_manager, DateFormatterInterface $date_formatter) { + $this->renderer = $renderer; + $this->urlGenerator = $url_generator; + $this->themeManager = $theme_manager; + $this->dateFormatter = $date_formatter; + } + + /** + * Sets the URL generator. + * + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator + * The URL generator. + * + * @return $this + * + * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. + */ + public function setGenerators(UrlGeneratorInterface $url_generator) { + return $this->setUrlGenerator($url_generator); + } + + /** + * Sets the URL generator. + * + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator + * The URL generator. + * + * @return $this + * + * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0. + */ + public function setUrlGenerator(UrlGeneratorInterface $url_generator) { + $this->urlGenerator = $url_generator; + return $this; + } + + /** + * Sets the theme manager. + * + * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager + * The theme manager. + * + * @return $this + * + * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0. + */ + public function setThemeManager(ThemeManagerInterface $theme_manager) { + $this->themeManager = $theme_manager; + return $this; + } + + /** + * Sets the date formatter. + * + * @param \Drupal\Core\Datetime\DateFormatter $date_formatter + * The date formatter. + * + * @return $this + * + * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0. + */ + public function setDateFormatter(DateFormatterInterface $date_formatter) { + $this->dateFormatter = $date_formatter; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() { + return [ + // This function will receive a renderable array, if an array is detected. + new \Twig_SimpleFunction('render_var', [$this, 'renderVar']), + // The url and path function are defined in close parallel to those found + // in \Symfony\Bridge\Twig\Extension\RoutingExtension + new \Twig_SimpleFunction('url', [$this, 'getUrl'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]), + new \Twig_SimpleFunction('path', [$this, 'getPath'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]), + new \Twig_SimpleFunction('link', [$this, 'getLink']), + new \Twig_SimpleFunction('file_url', function ($uri) { + return file_url_transform_relative(file_create_url($uri)); + }), + new \Twig_SimpleFunction('attach_library', [$this, 'attachLibrary']), + new \Twig_SimpleFunction('active_theme_path', [$this, 'getActiveThemePath']), + new \Twig_SimpleFunction('active_theme', [$this, 'getActiveTheme']), + new \Twig_SimpleFunction('create_attribute', [$this, 'createAttribute']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getFilters() { + return [ + // Translation filters. + new \Twig_SimpleFilter('t', 't', ['is_safe' => ['html']]), + new \Twig_SimpleFilter('trans', 't', ['is_safe' => ['html']]), + // The "raw" filter is not detectable when parsing "trans" tags. To detect + // which prefix must be used for translation (@, !, %), we must clone the + // "raw" filter and give it identifiable names. These filters should only + // be used in "trans" tags. + // @see TwigNodeTrans::compileString() + new \Twig_SimpleFilter('placeholder', [$this, 'escapePlaceholder'], ['is_safe' => ['html'], 'needs_environment' => TRUE]), + + // Replace twig's escape filter with our own. + new \Twig_SimpleFilter('drupal_escape', [$this, 'escapeFilter'], ['needs_environment' => TRUE, 'is_safe_callback' => 'twig_escape_filter_is_safe']), + + // Implements safe joining. + // @todo Make that the default for |join? Upstream issue: + // https://github.com/fabpot/Twig/issues/1420 + new \Twig_SimpleFilter('safe_join', [$this, 'safeJoin'], ['needs_environment' => TRUE, 'is_safe' => ['html']]), + + // Array filters. + new \Twig_SimpleFilter('without', 'twig_without'), + + // CSS class and ID filters. + new \Twig_SimpleFilter('clean_class', '\Drupal\Component\Utility\Html::getClass'), + new \Twig_SimpleFilter('clean_id', '\Drupal\Component\Utility\Html::getId'), + // This filter will render a renderable array to use the string results. + new \Twig_SimpleFilter('render', [$this, 'renderVar']), + new \Twig_SimpleFilter('format_date', [$this->dateFormatter, 'format']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getNodeVisitors() { + // The node visitor is needed to wrap all variables with + // render_var -> TwigExtension->renderVar() function. + return [ + new TwigNodeVisitor(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getTokenParsers() { + return [ + new TwigTransTokenParser(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getName() { + return 'drupal_core'; + } + + /** + * Generates a URL path given a route name and parameters. + * + * @param $name + * The name of the route. + * @param array $parameters + * An associative array of route parameters names and values. + * @param array $options + * (optional) An associative array of additional options. The 'absolute' + * option is forced to be FALSE. + * + * @return string + * The generated URL path (relative URL) for the given route. + * + * @see \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() + */ + public function getPath($name, $parameters = [], $options = []) { + $options['absolute'] = FALSE; + return $this->urlGenerator->generateFromRoute($name, $parameters, $options); + } + + /** + * Generates an absolute URL given a route name and parameters. + * + * @param $name + * The name of the route. + * @param array $parameters + * An associative array of route parameter names and values. + * @param array $options + * (optional) An associative array of additional options. The 'absolute' + * option is forced to be TRUE. + * + * @return string + * The generated absolute URL for the given route. + * + * @todo Add an option for scheme-relative URLs. + */ + public function getUrl($name, $parameters = [], $options = []) { + // Generate URL. + $options['absolute'] = TRUE; + $generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE); + + // Return as render array, so we can bubble the bubbleable metadata. + $build = ['#markup' => $generated_url->getGeneratedUrl()]; + $generated_url->applyTo($build); + return $build; + } + + /** + * Gets a rendered link from a url object. + * + * @param string $text + * The link text for the anchor tag as a translated string. + * @param \Drupal\Core\Url|string $url + * The URL object or string used for the link. + * @param array|\Drupal\Core\Template\Attribute $attributes + * An optional array or Attribute object of link attributes. + * + * @return array + * A render array representing a link to the given URL. + */ + public function getLink($text, $url, $attributes = []) { + if (!$url instanceof Url) { + $url = Url::fromUri($url); + } + // The twig extension should not modify the original URL object, this + // ensures consistent rendering. + // @see https://www.drupal.org/node/2842399 + $url = clone $url; + if ($attributes) { + if ($attributes instanceof Attribute) { + $attributes = $attributes->toArray(); + } + $url->mergeOptions(['attributes' => $attributes]); + } + // The text has been processed by twig already, convert it to a safe object + // for the render system. + if ($text instanceof \Twig_Markup) { + $text = Markup::create($text); + } + $build = [ + '#type' => 'link', + '#title' => $text, + '#url' => $url, + ]; + return $build; + } + + /** + * Gets the name of the active theme. + * + * @return string + * The name of the active theme. + */ + public function getActiveTheme() { + return $this->themeManager->getActiveTheme()->getName(); + } + + /** + * Gets the path of the active theme. + * + * @return string + * The path to the active theme. + */ + public function getActiveThemePath() { + return $this->themeManager->getActiveTheme()->getPath(); + } + + /** + * Determines at compile time whether the generated URL will be safe. + * + * Saves the unneeded automatic escaping for performance reasons. + * + * The URL generation process percent encodes non-alphanumeric characters. + * Thus, the only character within a URL that must be escaped in HTML is the + * ampersand ("&") which separates query params. Thus we cannot mark + * the generated URL as always safe, but only when we are sure there won't be + * multiple query params. This is the case when there are none or only one + * constant parameter given. For instance, we know beforehand this will not + * need to be escaped: + * - path('route') + * - path('route', {'param': 'value'}) + * But the following may need to be escaped: + * - path('route', var) + * - path('route', {'param': ['val1', 'val2'] }) // a sub-array + * - path('route', {'param1': 'value1', 'param2': 'value2'}) + * If param1 and param2 reference placeholders in the route, it would not + * need to be escaped, but we don't know that in advance. + * + * @param \Twig_Node $args_node + * The arguments of the path/url functions. + * + * @return array + * An array with the contexts the URL is safe + */ + public function isUrlGenerationSafe(\Twig_Node $args_node) { + // Support named arguments. + $parameter_node = $args_node->hasNode('parameters') ? $args_node->getNode('parameters') : ($args_node->hasNode(1) ? $args_node->getNode(1) : NULL); + + if (!isset($parameter_node) || $parameter_node instanceof \Twig_Node_Expression_Array && count($parameter_node) <= 2 && + (!$parameter_node->hasNode(1) || $parameter_node->getNode(1) instanceof \Twig_Node_Expression_Constant)) { + return ['html']; + } + + return []; + } + + /** + * Attaches an asset library to the template, and hence to the response. + * + * Allows Twig templates to attach asset libraries using + * @code + * {{ attach_library('extension/library_name') }} + * @endcode + * + * @param string $library + * An asset library. + */ + public function attachLibrary($library) { + // Use Renderer::render() on a temporary render array to get additional + // bubbleable metadata on the render stack. + $template_attached = ['#attached' => ['library' => [$library]]]; + $this->renderer->render($template_attached); + } + + /** + * Provides a placeholder wrapper around ::escapeFilter. + * + * @param \Twig_Environment $env + * A Twig_Environment instance. + * @param mixed $string + * The value to be escaped. + * + * @return string|null + * The escaped, rendered output, or NULL if there is no valid output. + */ + public function escapePlaceholder($env, $string) { + return '<em class="placeholder">' . $this->escapeFilter($env, $string) . '</em>'; + } + + /** + * Overrides twig_escape_filter(). + * + * Replacement function for Twig's escape filter. + * + * Note: This function should be kept in sync with + * theme_render_and_autoescape(). + * + * @param \Twig_Environment $env + * A Twig_Environment instance. + * @param mixed $arg + * The value to be escaped. + * @param string $strategy + * The escaping strategy. Defaults to 'html'. + * @param string $charset + * The charset. + * @param bool $autoescape + * Whether the function is called by the auto-escaping feature (TRUE) or by + * the developer (FALSE). + * + * @return string|null + * The escaped, rendered output, or NULL if there is no valid output. + * + * @throws \Exception + * When $arg is passed as an object which does not implement __toString(), + * RenderableInterface or toString(). + * + * @todo Refactor this to keep it in sync with theme_render_and_autoescape() + * in https://www.drupal.org/node/2575065 + */ + public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $charset = NULL, $autoescape = FALSE) { + // Check for a numeric zero int or float. + if ($arg === 0 || $arg === 0.0) { + return 0; + } + + // Return early for NULL and empty arrays. + if ($arg == NULL) { + return NULL; + } + + $this->bubbleArgMetadata($arg); + + // Keep Twig_Markup objects intact to support autoescaping. + if ($autoescape && ($arg instanceof \Twig_Markup || $arg instanceof MarkupInterface)) { + return $arg; + } + + $return = NULL; + + if (is_scalar($arg)) { + $return = (string) $arg; + } + elseif (is_object($arg)) { + if ($arg instanceof RenderableInterface) { + $arg = $arg->toRenderable(); + } + elseif (method_exists($arg, '__toString')) { + $return = (string) $arg; + } + // You can't throw exceptions in the magic PHP __toString() methods, see + // http://php.net/manual/language.oop5.magic.php#object.tostring so + // we also support a toString method. + elseif (method_exists($arg, 'toString')) { + $return = $arg->toString(); + } + else { + throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.'); + } + } + + // We have a string or an object converted to a string: Autoescape it! + if (isset($return)) { + if ($autoescape && $return instanceof MarkupInterface) { + return $return; + } + // Drupal only supports the HTML escaping strategy, so provide a + // fallback for other strategies. + if ($strategy == 'html') { + return Html::escape($return); + } + return twig_escape_filter($env, $return, $strategy, $charset, $autoescape); + } + + // This is a normal render array, which is safe by definition, with + // special simple cases already handled. + + // Early return if this element was pre-rendered (no need to re-render). + if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) { + return $arg['#markup']; + } + $arg['#printed'] = FALSE; + return $this->renderer->render($arg); + } + + /** + * Bubbles Twig template argument's cacheability & attachment metadata. + * + * For example: a generated link or generated URL object is passed as a Twig + * template argument, and its bubbleable metadata must be bubbled. + * + * @see \Drupal\Core\GeneratedLink + * @see \Drupal\Core\GeneratedUrl + * + * @param mixed $arg + * A Twig template argument that is about to be printed. + * + * @see \Drupal\Core\Theme\ThemeManager::render() + * @see \Drupal\Core\Render\RendererInterface::render() + */ + protected function bubbleArgMetadata($arg) { + // If it's a renderable, then it'll be up to the generated render array it + // returns to contain the necessary cacheability & attachment metadata. If + // it doesn't implement CacheableDependencyInterface or AttachmentsInterface + // then there is nothing to do here. + if ($arg instanceof RenderableInterface || !($arg instanceof CacheableDependencyInterface || $arg instanceof AttachmentsInterface)) { + return; + } + + $arg_bubbleable = []; + BubbleableMetadata::createFromObject($arg) + ->applyTo($arg_bubbleable); + + $this->renderer->render($arg_bubbleable); + } + + /** + * Wrapper around render() for twig printed output. + * + * If an object is passed which does not implement __toString(), + * RenderableInterface or toString() then an exception is thrown; + * Other objects are casted to string. However in the case that the + * object is an instance of a Twig_Markup object it is returned directly + * to support auto escaping. + * + * If an array is passed it is rendered via render() and scalar values are + * returned directly. + * + * @param mixed $arg + * String, Object or Render Array. + * + * @throws \Exception + * When $arg is passed as an object which does not implement __toString(), + * RenderableInterface or toString(). + * + * @return mixed + * The rendered output or an Twig_Markup object. + * + * @see render + * @see TwigNodeVisitor + */ + public function renderVar($arg) { + // Check for a numeric zero int or float. + if ($arg === 0 || $arg === 0.0) { + return 0; + } + + // Return early for NULL and empty arrays. + if ($arg == NULL) { + return NULL; + } + + // Optimize for scalars as it is likely they come from the escape filter. + if (is_scalar($arg)) { + return $arg; + } + + if (is_object($arg)) { + $this->bubbleArgMetadata($arg); + if ($arg instanceof RenderableInterface) { + $arg = $arg->toRenderable(); + } + elseif (method_exists($arg, '__toString')) { + return (string) $arg; + } + // You can't throw exceptions in the magic PHP __toString() methods, see + // http://php.net/manual/language.oop5.magic.php#object.tostring so + // we also support a toString method. + elseif (method_exists($arg, 'toString')) { + return $arg->toString(); + } + else { + throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.'); + } + } + + // This is a render array, with special simple cases already handled. + // Early return if this element was pre-rendered (no need to re-render). + if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) { + return $arg['#markup']; + } + $arg['#printed'] = FALSE; + return $this->renderer->render($arg); + } + + /** + * Joins several strings together safely. + * + * @param \Twig_Environment $env + * A Twig_Environment instance. + * @param mixed[]|\Traversable|null $value + * The pieces to join. + * @param string $glue + * The delimiter with which to join the string. Defaults to an empty string. + * This value is expected to be safe for output and user provided data + * should never be used as a glue. + * + * @return string + * The strings joined together. + */ + public function safeJoin(\Twig_Environment $env, $value, $glue = '') { + if ($value instanceof \Traversable) { + $value = iterator_to_array($value, FALSE); + } + + return implode($glue, array_map(function ($item) use ($env) { + // If $item is not marked safe then it will be escaped. + return $this->escapeFilter($env, $item, 'html', NULL, TRUE); + }, (array) $value)); + } + + /** + * Creates an Attribute object. + * + * @param array $attributes + * (optional) An associative array of key-value pairs to be converted to + * HTML attributes. + * + * @return \Drupal\Core\Template\Attribute + * An attributes object that has the given attributes. + */ + public function createAttribute(array $attributes = []) { + return new Attribute($attributes); + } + +}