TwigExtension.php 20 KB

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