comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
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 }