377efe6ddeffa6b4a6ffc3db19d11dba5ef9da46
[yaffs-website] / web / core / lib / Drupal / Core / Template / TwigExtension.php
1 <?php
2
3 namespace Drupal\Core\Template;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Render\MarkupInterface;
7 use Drupal\Core\Cache\CacheableDependencyInterface;
8 use Drupal\Core\Datetime\DateFormatterInterface;
9 use Drupal\Core\Render\AttachmentsInterface;
10 use Drupal\Core\Render\BubbleableMetadata;
11 use Drupal\Core\Render\Markup;
12 use Drupal\Core\Render\RenderableInterface;
13 use Drupal\Core\Render\RendererInterface;
14 use Drupal\Core\Routing\UrlGeneratorInterface;
15 use Drupal\Core\Theme\ThemeManagerInterface;
16 use Drupal\Core\Url;
17
18 /**
19  * A class providing Drupal Twig extensions.
20  *
21  * This provides a Twig extension that registers various Drupal-specific
22  * extensions to Twig, specifically Twig functions, filter, and node visitors.
23  *
24  * @see \Drupal\Core\CoreServiceProvider
25  */
26 class TwigExtension extends \Twig_Extension {
27
28   /**
29    * The URL generator.
30    *
31    * @var \Drupal\Core\Routing\UrlGeneratorInterface
32    */
33   protected $urlGenerator;
34
35   /**
36    * The renderer.
37    *
38    * @var \Drupal\Core\Render\RendererInterface
39    */
40   protected $renderer;
41
42   /**
43    * The theme manager.
44    *
45    * @var \Drupal\Core\Theme\ThemeManagerInterface
46    */
47   protected $themeManager;
48
49   /**
50    * The date formatter.
51    *
52    * @var \Drupal\Core\Datetime\DateFormatterInterface
53    */
54   protected $dateFormatter;
55
56   /**
57    * Constructs \Drupal\Core\Template\TwigExtension.
58    *
59    * @param \Drupal\Core\Render\RendererInterface $renderer
60    *   The renderer.
61    * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
62    *   The URL generator.
63    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
64    *   The theme manager.
65    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
66    *   The date formatter.
67    */
68   public function __construct(RendererInterface $renderer, UrlGeneratorInterface $url_generator, ThemeManagerInterface $theme_manager, DateFormatterInterface $date_formatter) {
69     $this->renderer = $renderer;
70     $this->urlGenerator = $url_generator;
71     $this->themeManager = $theme_manager;
72     $this->dateFormatter = $date_formatter;
73   }
74
75   /**
76    * Sets the URL generator.
77    *
78    * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
79    *   The URL generator.
80    *
81    * @return $this
82    *
83    * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
84    */
85   public function setGenerators(UrlGeneratorInterface $url_generator) {
86     return $this->setUrlGenerator($url_generator);
87   }
88
89   /**
90    * Sets the URL generator.
91    *
92    * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
93    *   The URL generator.
94    *
95    * @return $this
96    *
97    * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0.
98    */
99   public function setUrlGenerator(UrlGeneratorInterface $url_generator) {
100     $this->urlGenerator = $url_generator;
101     return $this;
102   }
103
104   /**
105    * Sets the theme manager.
106    *
107    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
108    *   The theme manager.
109    *
110    * @return $this
111    *
112    * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0.
113    */
114   public function setThemeManager(ThemeManagerInterface $theme_manager) {
115     $this->themeManager = $theme_manager;
116     return $this;
117   }
118
119   /**
120    * Sets the date formatter.
121    *
122    * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
123    *   The date formatter.
124    *
125    * @return $this
126    *
127    * @deprecated in Drupal 8.3.x-dev, will be removed before Drupal 9.0.0.
128    */
129   public function setDateFormatter(DateFormatterInterface $date_formatter) {
130     $this->dateFormatter = $date_formatter;
131     return $this;
132   }
133
134   /**
135    * {@inheritdoc}
136    */
137   public function getFunctions() {
138     return [
139       // This function will receive a renderable array, if an array is detected.
140       new \Twig_SimpleFunction('render_var', [$this, 'renderVar']),
141       // The url and path function are defined in close parallel to those found
142       // in \Symfony\Bridge\Twig\Extension\RoutingExtension
143       new \Twig_SimpleFunction('url', [$this, 'getUrl'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]),
144       new \Twig_SimpleFunction('path', [$this, 'getPath'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]),
145       new \Twig_SimpleFunction('link', [$this, 'getLink']),
146       new \Twig_SimpleFunction('file_url', function ($uri) {
147         return file_url_transform_relative(file_create_url($uri));
148       }),
149       new \Twig_SimpleFunction('attach_library', [$this, 'attachLibrary']),
150       new \Twig_SimpleFunction('active_theme_path', [$this, 'getActiveThemePath']),
151       new \Twig_SimpleFunction('active_theme', [$this, 'getActiveTheme']),
152       new \Twig_SimpleFunction('create_attribute', [$this, 'createAttribute']),
153     ];
154   }
155
156   /**
157    * {@inheritdoc}
158    */
159   public function getFilters() {
160     return [
161       // Translation filters.
162       new \Twig_SimpleFilter('t', 't', ['is_safe' => ['html']]),
163       new \Twig_SimpleFilter('trans', 't', ['is_safe' => ['html']]),
164       // The "raw" filter is not detectable when parsing "trans" tags. To detect
165       // which prefix must be used for translation (@, !, %), we must clone the
166       // "raw" filter and give it identifiable names. These filters should only
167       // be used in "trans" tags.
168       // @see TwigNodeTrans::compileString()
169       new \Twig_SimpleFilter('placeholder', [$this, 'escapePlaceholder'], ['is_safe' => ['html'], 'needs_environment' => TRUE]),
170
171       // Replace twig's escape filter with our own.
172       new \Twig_SimpleFilter('drupal_escape', [$this, 'escapeFilter'], ['needs_environment' => TRUE, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
173
174       // Implements safe joining.
175       // @todo Make that the default for |join? Upstream issue:
176       //   https://github.com/fabpot/Twig/issues/1420
177       new \Twig_SimpleFilter('safe_join', [$this, 'safeJoin'], ['needs_environment' => TRUE, 'is_safe' => ['html']]),
178
179       // Array filters.
180       new \Twig_SimpleFilter('without', 'twig_without'),
181
182       // CSS class and ID filters.
183       new \Twig_SimpleFilter('clean_class', '\Drupal\Component\Utility\Html::getClass'),
184       new \Twig_SimpleFilter('clean_id', '\Drupal\Component\Utility\Html::getId'),
185       // This filter will render a renderable array to use the string results.
186       new \Twig_SimpleFilter('render', [$this, 'renderVar']),
187       new \Twig_SimpleFilter('format_date', [$this->dateFormatter, 'format']),
188     ];
189   }
190
191   /**
192    * {@inheritdoc}
193    */
194   public function getNodeVisitors() {
195     // The node visitor is needed to wrap all variables with
196     // render_var -> TwigExtension->renderVar() function.
197     return [
198       new TwigNodeVisitor(),
199     ];
200   }
201
202   /**
203    * {@inheritdoc}
204    */
205   public function getTokenParsers() {
206     return [
207       new TwigTransTokenParser(),
208     ];
209   }
210
211   /**
212    * {@inheritdoc}
213    */
214   public function getName() {
215     return 'drupal_core';
216   }
217
218   /**
219    * Generates a URL path given a route name and parameters.
220    *
221    * @param $name
222    *   The name of the route.
223    * @param array $parameters
224    *   An associative array of route parameters names and values.
225    * @param array $options
226    *   (optional) An associative array of additional options. The 'absolute'
227    *   option is forced to be FALSE.
228    *
229    * @return string
230    *   The generated URL path (relative URL) for the given route.
231    *
232    * @see \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute()
233    */
234   public function getPath($name, $parameters = [], $options = []) {
235     $options['absolute'] = FALSE;
236     return $this->urlGenerator->generateFromRoute($name, $parameters, $options);
237   }
238
239   /**
240    * Generates an absolute URL given a route name and parameters.
241    *
242    * @param $name
243    *   The name of the route.
244    * @param array $parameters
245    *   An associative array of route parameter names and values.
246    * @param array $options
247    *   (optional) An associative array of additional options. The 'absolute'
248    *   option is forced to be TRUE.
249    *
250    * @return string
251    *   The generated absolute URL for the given route.
252    *
253    * @todo Add an option for scheme-relative URLs.
254    */
255   public function getUrl($name, $parameters = [], $options = []) {
256     // Generate URL.
257     $options['absolute'] = TRUE;
258     $generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
259
260     // Return as render array, so we can bubble the bubbleable metadata.
261     $build = ['#markup' => $generated_url->getGeneratedUrl()];
262     $generated_url->applyTo($build);
263     return $build;
264   }
265
266   /**
267    * Gets a rendered link from a url object.
268    *
269    * @param string $text
270    *   The link text for the anchor tag as a translated string.
271    * @param \Drupal\Core\Url|string $url
272    *   The URL object or string used for the link.
273    * @param array|\Drupal\Core\Template\Attribute $attributes
274    *   An optional array or Attribute object of link attributes.
275    *
276    * @return array
277    *   A render array representing a link to the given URL.
278    */
279   public function getLink($text, $url, $attributes = []) {
280     if (!$url instanceof Url) {
281       $url = Url::fromUri($url);
282     }
283     // The twig extension should not modify the original URL object, this
284     // ensures consistent rendering.
285     // @see https://www.drupal.org/node/2842399
286     $url = clone $url;
287     if ($attributes) {
288       if ($attributes instanceof Attribute) {
289         $attributes = $attributes->toArray();
290       }
291       $url->mergeOptions(['attributes' => $attributes]);
292     }
293     // The text has been processed by twig already, convert it to a safe object
294     // for the render system.
295     if ($text instanceof \Twig_Markup) {
296       $text = Markup::create($text);
297     }
298     $build = [
299       '#type' => 'link',
300       '#title' => $text,
301       '#url' => $url,
302     ];
303     return $build;
304   }
305
306   /**
307    * Gets the name of the active theme.
308    *
309    * @return string
310    *   The name of the active theme.
311    */
312   public function getActiveTheme() {
313     return $this->themeManager->getActiveTheme()->getName();
314   }
315
316   /**
317    * Gets the path of the active theme.
318    *
319    * @return string
320    *   The path to the active theme.
321    */
322   public function getActiveThemePath() {
323     return $this->themeManager->getActiveTheme()->getPath();
324   }
325
326   /**
327    * Determines at compile time whether the generated URL will be safe.
328    *
329    * Saves the unneeded automatic escaping for performance reasons.
330    *
331    * The URL generation process percent encodes non-alphanumeric characters.
332    * Thus, the only character within a URL that must be escaped in HTML is the
333    * ampersand ("&") which separates query params. Thus we cannot mark
334    * the generated URL as always safe, but only when we are sure there won't be
335    * multiple query params. This is the case when there are none or only one
336    * constant parameter given. For instance, we know beforehand this will not
337    * need to be escaped:
338    * - path('route')
339    * - path('route', {'param': 'value'})
340    * But the following may need to be escaped:
341    * - path('route', var)
342    * - path('route', {'param': ['val1', 'val2'] }) // a sub-array
343    * - path('route', {'param1': 'value1', 'param2': 'value2'})
344    * If param1 and param2 reference placeholders in the route, it would not
345    * need to be escaped, but we don't know that in advance.
346    *
347    * @param \Twig_Node $args_node
348    *   The arguments of the path/url functions.
349    *
350    * @return array
351    *   An array with the contexts the URL is safe
352    */
353   public function isUrlGenerationSafe(\Twig_Node $args_node) {
354     // Support named arguments.
355     $parameter_node = $args_node->hasNode('parameters') ? $args_node->getNode('parameters') : ($args_node->hasNode(1) ? $args_node->getNode(1) : NULL);
356
357     if (!isset($parameter_node) || $parameter_node instanceof \Twig_Node_Expression_Array && count($parameter_node) <= 2 &&
358         (!$parameter_node->hasNode(1) || $parameter_node->getNode(1) instanceof \Twig_Node_Expression_Constant)) {
359       return ['html'];
360     }
361
362     return [];
363   }
364
365   /**
366    * Attaches an asset library to the template, and hence to the response.
367    *
368    * Allows Twig templates to attach asset libraries using
369    * @code
370    * {{ attach_library('extension/library_name') }}
371    * @endcode
372    *
373    * @param string $library
374    *   An asset library.
375    */
376   public function attachLibrary($library) {
377     // Use Renderer::render() on a temporary render array to get additional
378     // bubbleable metadata on the render stack.
379     $template_attached = ['#attached' => ['library' => [$library]]];
380     $this->renderer->render($template_attached);
381   }
382
383   /**
384    * Provides a placeholder wrapper around ::escapeFilter.
385    *
386    * @param \Twig_Environment $env
387    *   A Twig_Environment instance.
388    * @param mixed $string
389    *   The value to be escaped.
390    *
391    * @return string|null
392    *   The escaped, rendered output, or NULL if there is no valid output.
393    */
394   public function escapePlaceholder($env, $string) {
395     return '<em class="placeholder">' . $this->escapeFilter($env, $string) . '</em>';
396   }
397
398   /**
399    * Overrides twig_escape_filter().
400    *
401    * Replacement function for Twig's escape filter.
402    *
403    * Note: This function should be kept in sync with
404    * theme_render_and_autoescape().
405    *
406    * @param \Twig_Environment $env
407    *   A Twig_Environment instance.
408    * @param mixed $arg
409    *   The value to be escaped.
410    * @param string $strategy
411    *   The escaping strategy. Defaults to 'html'.
412    * @param string $charset
413    *   The charset.
414    * @param bool $autoescape
415    *   Whether the function is called by the auto-escaping feature (TRUE) or by
416    *   the developer (FALSE).
417    *
418    * @return string|null
419    *   The escaped, rendered output, or NULL if there is no valid output.
420    *
421    * @throws \Exception
422    *   When $arg is passed as an object which does not implement __toString(),
423    *   RenderableInterface or toString().
424    *
425    * @todo Refactor this to keep it in sync with theme_render_and_autoescape()
426    *   in https://www.drupal.org/node/2575065
427    */
428   public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $charset = NULL, $autoescape = FALSE) {
429     // Check for a numeric zero int or float.
430     if ($arg === 0 || $arg === 0.0) {
431       return 0;
432     }
433
434     // Return early for NULL and empty arrays.
435     if ($arg == NULL) {
436       return NULL;
437     }
438
439     $this->bubbleArgMetadata($arg);
440
441     // Keep Twig_Markup objects intact to support autoescaping.
442     if ($autoescape && ($arg instanceof \Twig_Markup || $arg instanceof MarkupInterface)) {
443       return $arg;
444     }
445
446     $return = NULL;
447
448     if (is_scalar($arg)) {
449       $return = (string) $arg;
450     }
451     elseif (is_object($arg)) {
452       if ($arg instanceof RenderableInterface) {
453         $arg = $arg->toRenderable();
454       }
455       elseif (method_exists($arg, '__toString')) {
456         $return = (string) $arg;
457       }
458       // You can't throw exceptions in the magic PHP __toString() methods, see
459       // http://php.net/manual/language.oop5.magic.php#object.tostring so
460       // we also support a toString method.
461       elseif (method_exists($arg, 'toString')) {
462         $return = $arg->toString();
463       }
464       else {
465         throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.');
466       }
467     }
468
469     // We have a string or an object converted to a string: Autoescape it!
470     if (isset($return)) {
471       if ($autoescape && $return instanceof MarkupInterface) {
472         return $return;
473       }
474       // Drupal only supports the HTML escaping strategy, so provide a
475       // fallback for other strategies.
476       if ($strategy == 'html') {
477         return Html::escape($return);
478       }
479       return twig_escape_filter($env, $return, $strategy, $charset, $autoescape);
480     }
481
482     // This is a normal render array, which is safe by definition, with
483     // special simple cases already handled.
484
485     // Early return if this element was pre-rendered (no need to re-render).
486     if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) {
487       return $arg['#markup'];
488     }
489     $arg['#printed'] = FALSE;
490     return $this->renderer->render($arg);
491   }
492
493   /**
494    * Bubbles Twig template argument's cacheability & attachment metadata.
495    *
496    * For example: a generated link or generated URL object is passed as a Twig
497    * template argument, and its bubbleable metadata must be bubbled.
498    *
499    * @see \Drupal\Core\GeneratedLink
500    * @see \Drupal\Core\GeneratedUrl
501    *
502    * @param mixed $arg
503    *   A Twig template argument that is about to be printed.
504    *
505    * @see \Drupal\Core\Theme\ThemeManager::render()
506    * @see \Drupal\Core\Render\RendererInterface::render()
507    */
508   protected function bubbleArgMetadata($arg) {
509     // If it's a renderable, then it'll be up to the generated render array it
510     // returns to contain the necessary cacheability & attachment metadata. If
511     // it doesn't implement CacheableDependencyInterface or AttachmentsInterface
512     // then there is nothing to do here.
513     if ($arg instanceof RenderableInterface || !($arg instanceof CacheableDependencyInterface || $arg instanceof AttachmentsInterface)) {
514       return;
515     }
516
517     $arg_bubbleable = [];
518     BubbleableMetadata::createFromObject($arg)
519       ->applyTo($arg_bubbleable);
520
521     $this->renderer->render($arg_bubbleable);
522   }
523
524   /**
525    * Wrapper around render() for twig printed output.
526    *
527    * If an object is passed which does not implement __toString(),
528    * RenderableInterface or toString() then an exception is thrown;
529    * Other objects are casted to string. However in the case that the
530    * object is an instance of a Twig_Markup object it is returned directly
531    * to support auto escaping.
532    *
533    * If an array is passed it is rendered via render() and scalar values are
534    * returned directly.
535    *
536    * @param mixed $arg
537    *   String, Object or Render Array.
538    *
539    * @throws \Exception
540    *   When $arg is passed as an object which does not implement __toString(),
541    *   RenderableInterface or toString().
542    *
543    * @return mixed
544    *   The rendered output or an Twig_Markup object.
545    *
546    * @see render
547    * @see TwigNodeVisitor
548    */
549   public function renderVar($arg) {
550     // Check for a numeric zero int or float.
551     if ($arg === 0 || $arg === 0.0) {
552       return 0;
553     }
554
555     // Return early for NULL and empty arrays.
556     if ($arg == NULL) {
557       return NULL;
558     }
559
560     // Optimize for scalars as it is likely they come from the escape filter.
561     if (is_scalar($arg)) {
562       return $arg;
563     }
564
565     if (is_object($arg)) {
566       $this->bubbleArgMetadata($arg);
567       if ($arg instanceof RenderableInterface) {
568         $arg = $arg->toRenderable();
569       }
570       elseif (method_exists($arg, '__toString')) {
571         return (string) $arg;
572       }
573       // You can't throw exceptions in the magic PHP __toString() methods, see
574       // http://php.net/manual/language.oop5.magic.php#object.tostring so
575       // we also support a toString method.
576       elseif (method_exists($arg, 'toString')) {
577         return $arg->toString();
578       }
579       else {
580         throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.');
581       }
582     }
583
584     // This is a render array, with special simple cases already handled.
585     // Early return if this element was pre-rendered (no need to re-render).
586     if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) {
587       return $arg['#markup'];
588     }
589     $arg['#printed'] = FALSE;
590     return $this->renderer->render($arg);
591   }
592
593   /**
594    * Joins several strings together safely.
595    *
596    * @param \Twig_Environment $env
597    *   A Twig_Environment instance.
598    * @param mixed[]|\Traversable|null $value
599    *   The pieces to join.
600    * @param string $glue
601    *   The delimiter with which to join the string. Defaults to an empty string.
602    *   This value is expected to be safe for output and user provided data
603    *   should never be used as a glue.
604    *
605    * @return string
606    *   The strings joined together.
607    */
608   public function safeJoin(\Twig_Environment $env, $value, $glue = '') {
609     if ($value instanceof \Traversable) {
610       $value = iterator_to_array($value, FALSE);
611     }
612
613     return implode($glue, array_map(function ($item) use ($env) {
614       // If $item is not marked safe then it will be escaped.
615       return $this->escapeFilter($env, $item, 'html', NULL, TRUE);
616     }, (array) $value));
617   }
618
619   /**
620    * Creates an Attribute object.
621    *
622    * @param array $attributes
623    *   (optional) An associative array of key-value pairs to be converted to
624    *   HTML attributes.
625    *
626    * @return \Drupal\Core\Template\Attribute
627    *   An attributes object that has the given attributes.
628    */
629   public function createAttribute(array $attributes = []) {
630     return new Attribute($attributes);
631   }
632
633 }