vendor/pimcore/pimcore/lib/Twig/Extension/Templating/HeadLink.php line 361

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. /**
  15.  * ----------------------------------------------------------------------------------
  16.  * based on @author ZF1 Zend_View_Helper_HeadLink
  17.  * ----------------------------------------------------------------------------------
  18.  */
  19. /**
  20.  * Zend Framework
  21.  *
  22.  * LICENSE
  23.  *
  24.  * This source file is subject to the new BSD license that is bundled
  25.  * with this package in the file LICENSE.txt.
  26.  * It is also available through the world-wide-web at this URL:
  27.  * http://framework.zend.com/license/new-bsd
  28.  * If you did not receive a copy of the license and are unable to
  29.  * obtain it through the world-wide-web, please send an email
  30.  * to license@zend.com so we can send you a copy immediately.
  31.  *
  32.  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
  33.  * @license    http://framework.zend.com/license/new-bsd     New BSD License
  34.  */
  35. namespace Pimcore\Twig\Extension\Templating;
  36. use Pimcore\Event\FrontendEvents;
  37. use Pimcore\Twig\Extension\Templating\Placeholder\CacheBusterAware;
  38. use Pimcore\Twig\Extension\Templating\Placeholder\Container;
  39. use Pimcore\Twig\Extension\Templating\Placeholder\ContainerService;
  40. use Pimcore\Twig\Extension\Templating\Placeholder\Exception;
  41. use Pimcore\Twig\Extension\Templating\Traits\WebLinksTrait;
  42. use Symfony\Bridge\Twig\Extension\WebLinkExtension;
  43. use Symfony\Component\EventDispatcher\GenericEvent;
  44. /**
  45.  * HeadLink
  46.  *
  47.  * @see        http://www.w3.org/TR/xhtml1/dtds.html
  48.  *
  49.  * @method $this appendAlternate($href, $type, $title, $extras)
  50.  * @method $this appendStylesheet($href, $media = 'screen', $conditionalStylesheet = false, array $extras = array())
  51.  * @method $this offsetSetAlternate($index, $href, $type, $title, $extras)
  52.  * @method $this offsetSetStylesheet($index, $href, $media = 'screen', $conditionalStylesheet = false, array $extras = array())
  53.  * @method $this prependAlternate($href, $type, $title, $extras)
  54.  * @method $this prependStylesheet($href, $media = 'screen', $conditionalStylesheet = false, array $extras = array())
  55.  * @method $this setAlternate($href, $type, $title, $extras)
  56.  * @method $this setStylesheet($href, $media = 'screen', $conditionalStylesheet = false, array $extras = array())
  57.  *
  58.  */
  59. class HeadLink extends CacheBusterAware
  60. {
  61.     use WebLinksTrait;
  62.     /**
  63.      * $_validAttributes
  64.      *
  65.      * @var array
  66.      */
  67.     protected $_itemKeys = [
  68.         'charset',
  69.         'href',
  70.         'hreflang',
  71.         'id',
  72.         'media',
  73.         'rel',
  74.         'rev',
  75.         'type',
  76.         'title',
  77.         'extras',
  78.         'sizes',
  79.     ];
  80.     /**
  81.      * @var string registry key
  82.      */
  83.     protected $_regKey 'HeadLink';
  84.     /**
  85.      * Default attributes for generated WebLinks (HTTP/2 push).
  86.      *
  87.      * @var array
  88.      */
  89.     protected $webLinkAttributes = ['as' => 'style'];
  90.     /**
  91.      * HeadLink constructor.
  92.      *
  93.      * Use PHP_EOL as separator
  94.      *
  95.      * @param ContainerService $containerService
  96.      * @param WebLinkExtension $webLinkExtension
  97.      */
  98.     public function __construct(
  99.         ContainerService $containerService,
  100.         WebLinkExtension $webLinkExtension
  101.     ) {
  102.         parent::__construct($containerService);
  103.         $this->webLinkExtension $webLinkExtension;
  104.         $this->setSeparator(PHP_EOL);
  105.     }
  106.     /**
  107.      * headLink() - View Helper Method
  108.      *
  109.      * Returns current object instance. Optionally, allows passing array of
  110.      * values to build link.
  111.      *
  112.      * @return HeadLink
  113.      */
  114.     public function __invoke(array $attributes null$placement Container::APPEND)
  115.     {
  116.         if (null !== $attributes) {
  117.             $item $this->createData($attributes);
  118.             switch ($placement) {
  119.                 case Container::SET:
  120.                     $this->set($item);
  121.                     break;
  122.                 case Container::PREPEND:
  123.                     $this->prepend($item);
  124.                     break;
  125.                 case Container::APPEND:
  126.                 default:
  127.                     $this->append($item);
  128.                     break;
  129.             }
  130.         }
  131.         return $this;
  132.     }
  133.     /**
  134.      * Overload method access
  135.      *
  136.      * Creates the following virtual methods:
  137.      * - appendStylesheet($href, $media, $conditionalStylesheet, $extras)
  138.      * - offsetSetStylesheet($index, $href, $media, $conditionalStylesheet, $extras)
  139.      * - prependStylesheet($href, $media, $conditionalStylesheet, $extras)
  140.      * - setStylesheet($href, $media, $conditionalStylesheet, $extras)
  141.      * - appendAlternate($href, $type, $title, $extras)
  142.      * - offsetSetAlternate($index, $href, $type, $title, $extras)
  143.      * - prependAlternate($href, $type, $title, $extras)
  144.      * - setAlternate($href, $type, $title, $extras)
  145.      *
  146.      * @param mixed $method
  147.      * @param mixed $args
  148.      *
  149.      * @return mixed
  150.      */
  151.     public function __call($method$args)
  152.     {
  153.         if (preg_match('/^(?P<action>set|(ap|pre)pend|offsetSet)(?P<type>Stylesheet|Alternate)$/'$method$matches)) {
  154.             $argc count($args);
  155.             $action $matches['action'];
  156.             $type $matches['type'];
  157.             $index null;
  158.             if ('offsetSet' == $action) {
  159.                 if ($argc) {
  160.                     $index array_shift($args);
  161.                     --$argc;
  162.                 }
  163.             }
  164.             if ($argc) {
  165.                 throw new Exception(sprintf('%s requires at least one argument'$method));
  166.             }
  167.             if (is_array($args[0])) {
  168.                 $item $this->createData($args[0]);
  169.             } else {
  170.                 $dataMethod 'createData' $type;
  171.                 $item $this->$dataMethod($args);
  172.             }
  173.             if ($item) {
  174.                 if ('offsetSet' == $action) {
  175.                     $this->offsetSet($index$item);
  176.                 } else {
  177.                     $this->$action($item);
  178.                 }
  179.             }
  180.             return $this;
  181.         }
  182.         return parent::__call($method$args);
  183.     }
  184.     /**
  185.      * Check if value is valid
  186.      *
  187.      * @param  mixed $value
  188.      *
  189.      * @return bool
  190.      */
  191.     protected function _isValid($value)
  192.     {
  193.         if (!$value instanceof \stdClass) {
  194.             return false;
  195.         }
  196.         $vars get_object_vars($value);
  197.         $keys array_keys($vars);
  198.         $intersection array_intersect($this->_itemKeys$keys);
  199.         if (empty($intersection)) {
  200.             return false;
  201.         }
  202.         return true;
  203.     }
  204.     /**
  205.      * append()
  206.      *
  207.      * @param  array $value
  208.      *
  209.      * @return void
  210.      */
  211.     public function append($value)
  212.     {
  213.         if (!$this->_isValid($value)) {
  214.             throw new Exception('append() expects a data token; please use one of the custom append*() methods');
  215.         }
  216.         $this->getContainer()->append($value);
  217.     }
  218.     /**
  219.      * offsetSet()
  220.      *
  221.      * @param  string|int $index
  222.      * @param  array $value
  223.      *
  224.      * @return void
  225.      */
  226.     public function offsetSet($index$value)
  227.     {
  228.         if (!$this->_isValid($value)) {
  229.             throw new Exception('offsetSet() expects a data token; please use one of the custom offsetSet*() methods');
  230.         }
  231.         $this->getContainer()->offsetSet($index$value);
  232.     }
  233.     /**
  234.      * prepend()
  235.      *
  236.      * @param array $value
  237.      */
  238.     public function prepend($value)
  239.     {
  240.         if (!$this->_isValid($value)) {
  241.             throw new Exception('prepend() expects a data token; please use one of the custom prepend*() methods');
  242.         }
  243.         $this->getContainer()->prepend($value);
  244.     }
  245.     /**
  246.      * set()
  247.      *
  248.      * @param array $value
  249.      */
  250.     public function set($value)
  251.     {
  252.         if (!$this->_isValid($value)) {
  253.             throw new Exception('set() expects a data token; please use one of the custom set*() methods');
  254.         }
  255.         $this->getContainer()->set($value);
  256.     }
  257.     /**
  258.      * Create HTML link element from data item
  259.      *
  260.      * @param  \stdClass $item
  261.      *
  262.      * @return string
  263.      */
  264.     public function itemToString(\stdClass $item)
  265.     {
  266.         $attributes = (array) $item;
  267.         $link '<link ';
  268.         foreach ($this->_itemKeys as $itemKey) {
  269.             if (isset($attributes[$itemKey])) {
  270.                 if (is_array($attributes[$itemKey])) {
  271.                     foreach ($attributes[$itemKey] as $key => $value) {
  272.                         $link .= sprintf('%s="%s" '$key, ($this->_autoEscape) ? $this->_escape($value) : $value);
  273.                     }
  274.                 } else {
  275.                     $link .= sprintf('%s="%s" '$itemKey, ($this->_autoEscape) ? $this->_escape($attributes[$itemKey]) : $attributes[$itemKey]);
  276.                 }
  277.             }
  278.         }
  279.         $link .= '/>';
  280.         if (($link == '<link />') || ($link == '<link >')) {
  281.             return '';
  282.         }
  283.         if (isset($attributes['conditionalStylesheet'])
  284.             && !empty($attributes['conditionalStylesheet'])
  285.             && is_string($attributes['conditionalStylesheet'])) {
  286.             if (str_replace(' '''$attributes['conditionalStylesheet']) === '!IE') {
  287.                 $link '<!-->' $link '<!--';
  288.             }
  289.             $link '<!--[if ' $attributes['conditionalStylesheet'] . ']>' $link '<![endif]-->';
  290.         }
  291.         return $link;
  292.     }
  293.     /**
  294.      * Render link elements as string
  295.      *
  296.      * @param  string|int $indent
  297.      *
  298.      * @return string
  299.      */
  300.     public function toString($indent null)
  301.     {
  302.         $this->prepareEntries();
  303.         $indent = (null !== $indent)
  304.             ? $this->getWhitespace($indent)
  305.             : $this->getIndent();
  306.         $items = [];
  307.         $this->getContainer()->ksort();
  308.         foreach ($this as $item) {
  309.             $items[] = $this->itemToString($item);
  310.         }
  311.         return $indent implode($this->_escape($this->getSeparator()) . $indent$items);
  312.     }
  313.     /**
  314.      * prepares entries with cache buster prefix
  315.      */
  316.     protected function prepareEntries()
  317.     {
  318.         foreach ($this as &$item) {
  319.             if ($this->isCacheBuster()) {
  320.                 // adds the automatic cache buster functionality
  321.                 if (isset($item->href)) {
  322.                     $realFile PIMCORE_WEB_ROOT $item->href;
  323.                     if (file_exists($realFile)) {
  324.                         $item->href '/cache-buster-' filemtime($realFile) . $item->href;
  325.                     }
  326.                 }
  327.             }
  328.             $event = new GenericEvent($this, [
  329.                 'item' => $item,
  330.             ]);
  331.             \Pimcore::getEventDispatcher()->dispatch(FrontendEvents::VIEW_HELPER_HEAD_LINK$event);
  332.             $source = (string)($item->href ?? '');
  333.             $itemAttributes = isset($item->extras) ? $item->extras : [];
  334.             if (isset($item->extras) && is_array($item->extras) && isset($item->extras['webLink'])) {
  335.                 unset($item->extras['webLink']);
  336.             }
  337.             if (is_array($itemAttributes) && !empty($source)) {
  338.                 $this->handleWebLink($item$source$itemAttributes);
  339.             }
  340.         }
  341.     }
  342.     /**
  343.      * Create data item for stack
  344.      *
  345.      * @param  array $attributes
  346.      *
  347.      * @return \stdClass
  348.      */
  349.     public function createData(array $attributes)
  350.     {
  351.         $data = (object) $attributes;
  352.         return $data;
  353.     }
  354.     /**
  355.      * Create item for stylesheet link item
  356.      *
  357.      * @param  array $args
  358.      *
  359.      * @return \stdClass|false Returns fals if stylesheet is a duplicate
  360.      */
  361.     public function createDataStylesheet(array $args)
  362.     {
  363.         $rel 'stylesheet';
  364.         $type 'text/css';
  365.         $media 'screen';
  366.         $conditionalStylesheet false;
  367.         $extras = [];
  368.         $href array_shift($args);
  369.         if ($this->_isDuplicateStylesheet($href)) {
  370.             return false;
  371.         }
  372.         if (count($args)) {
  373.             $media array_shift($args);
  374.             if (is_array($media)) {
  375.                 $media implode(','$media);
  376.             } else {
  377.                 $media = (string) $media;
  378.             }
  379.         }
  380.         if (count($args)) {
  381.             $conditionalStylesheet array_shift($args);
  382.             if (!empty($conditionalStylesheet) && is_string($conditionalStylesheet)) {
  383.                 $conditionalStylesheet = (string) $conditionalStylesheet;
  384.             } else {
  385.                 $conditionalStylesheet null;
  386.             }
  387.         }
  388.         if (count($args) && is_array($args[0])) {
  389.             $extras array_shift($args);
  390.             $extras = (array) $extras;
  391.         }
  392.         $attributes compact('rel''type''href''media''conditionalStylesheet''extras');
  393.         return $this->createData($this->_applyExtras($attributes));
  394.     }
  395.     /**
  396.      * Is the linked stylesheet a duplicate?
  397.      *
  398.      * @param  string $uri
  399.      *
  400.      * @return bool
  401.      */
  402.     protected function _isDuplicateStylesheet($uri)
  403.     {
  404.         foreach ($this->getContainer() as $item) {
  405.             if (($item->rel == 'stylesheet') && ($item->href == $uri)) {
  406.                 return true;
  407.             }
  408.         }
  409.         return false;
  410.     }
  411.     /**
  412.      * Create item for alternate link item
  413.      *
  414.      * @param  array $args
  415.      *
  416.      * @return \stdClass
  417.      */
  418.     public function createDataAlternate(array $args)
  419.     {
  420.         if (count($args)) {
  421.             throw new Exception(sprintf('Alternate tags require 3 arguments; %s provided'count($args)));
  422.         }
  423.         $rel 'alternate';
  424.         $href array_shift($args);
  425.         $type array_shift($args);
  426.         $title array_shift($args);
  427.         $extras = [];
  428.         if (count($args) && is_array($args[0])) {
  429.             $extras array_shift($args);
  430.             $extras = (array) $extras;
  431.             if (isset($extras['media']) && is_array($extras['media'])) {
  432.                 $extras['media'] = implode(','$extras['media']);
  433.             }
  434.         }
  435.         $href = (string) $href;
  436.         $type = (string) $type;
  437.         $title = (string) $title;
  438.         $attributes compact('rel''href''type''title''extras');
  439.         return $this->createData($this->_applyExtras($attributes));
  440.     }
  441.     /**
  442.      * Apply any overrides specified in the 'extras' array
  443.      *
  444.      * @param array $attributes
  445.      *
  446.      * @return array
  447.      */
  448.     protected function _applyExtras($attributes)
  449.     {
  450.         if (isset($attributes['extras'])) {
  451.             foreach ($attributes['extras'] as $eKey => $eVal) {
  452.                 if (isset($attributes[$eKey])) {
  453.                     $attributes[$eKey] = $eVal;
  454.                     unset($attributes['extras'][$eKey]);
  455.                 }
  456.             }
  457.         }
  458.         return $attributes;
  459.     }
  460. }
  461. class_alias(HeadLink::class, 'Pimcore\Templating\Helper\HeadLink');