vendor/pimcore/pimcore/lib/Twig/Extension/Templating/HeadScript.php line 522

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_HeadScript
  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. use Twig\Extension\RuntimeExtensionInterface;
  45. /**
  46.  * @method $this appendFile($src, $type = 'text/javascript', array $attrs = array())
  47.  * @method $this appendScript($script, $type = 'text/javascript', array $attrs = array())
  48.  * @method $this offsetSetFile($index, $src, $type = 'text/javascript', array $attrs = array())
  49.  * @method $this offsetSetScript($index, $script, $type = 'text/javascript', array $attrs = array())
  50.  * @method $this prependFile($src, $type = 'text/javascript', array $attrs = array())
  51.  * @method $this prependScript($script, $type = 'text/javascript', array $attrs = array())
  52.  * @method $this setFile($src, $type = 'text/javascript', array $attrs = array())
  53.  * @method $this setScript($script, $type = 'text/javascript', array $attrs = array())
  54.  *
  55.  */
  56. class HeadScript extends CacheBusterAware implements RuntimeExtensionInterface
  57. {
  58.     use WebLinksTrait;
  59.     /**#@+
  60.      * Script type contants
  61.      * @const string
  62.      */
  63.     const FILE 'FILE';
  64.     const SCRIPT 'SCRIPT';
  65.     // #@-
  66.     /**
  67.      * Registry key for placeholder
  68.      *
  69.      * @var string
  70.      */
  71.     protected $_regKey 'HeadScript';
  72.     /**
  73.      * Are arbitrary attributes allowed?
  74.      *
  75.      * @var bool
  76.      */
  77.     protected $_arbitraryAttributes false;
  78.     /**#@+
  79.      * Capture type and/or attributes (used for hinting during capture)
  80.      * @var string
  81.      */
  82.     protected $_captureLock;
  83.     protected $_captureScriptType null;
  84.     protected $_captureScriptAttrs null;
  85.     protected $_captureType;
  86.     // #@-
  87.     /**
  88.      * Optional allowed attributes for script tag
  89.      *
  90.      * @var array
  91.      */
  92.     protected $_optionalAttributes = [
  93.         'charset''defer''language''src''type',
  94.     ];
  95.     /**
  96.      * Required attributes for script tag
  97.      *
  98.      * @var array
  99.      */
  100.     protected $_requiredAttributes = ['type'];
  101.     /**
  102.      * Whether or not to format scripts using CDATA; used only if doctype
  103.      * helper is not accessible
  104.      *
  105.      * @var bool
  106.      */
  107.     public $useCdata false;
  108.     /**
  109.      * Default attributes for generated WebLinks (HTTP/2 push).
  110.      *
  111.      * @var array
  112.      */
  113.     protected $webLinkAttributes = ['as' => 'script'];
  114.     /**
  115.      * HeadScript constructor.
  116.      *
  117.      * Set separator to PHP_EOL.
  118.      *
  119.      * @param ContainerService $containerService
  120.      * @param WebLinkExtension $webLinkExtension
  121.      */
  122.     public function __construct(
  123.         ContainerService $containerService,
  124.         WebLinkExtension $webLinkExtension
  125.     ) {
  126.         parent::__construct($containerService);
  127.         $this->webLinkExtension $webLinkExtension;
  128.         $this->setSeparator(PHP_EOL);
  129.     }
  130.     /**
  131.      * Return headScript object
  132.      *
  133.      * Returns headScript helper object; optionally, allows specifying a script
  134.      * or script file to include.
  135.      *
  136.      * @param  string $mode Script or file
  137.      * @param  string $spec Script/url
  138.      * @param  string $placement Append, prepend, or set
  139.      * @param  array $attrs Array of script attributes
  140.      * @param  string $type Script type and/or array of script attributes
  141.      *
  142.      * @return HeadScript
  143.      */
  144.     public function __invoke($mode self::FILE$spec null$placement 'APPEND', array $attrs = [], $type 'text/javascript')
  145.     {
  146.         if ((null !== $spec) && is_string($spec)) {
  147.             $action ucfirst(strtolower($mode));
  148.             $placement strtolower($placement);
  149.             switch ($placement) {
  150.                 case 'set':
  151.                 case 'prepend':
  152.                 case 'append':
  153.                     $action $placement $action;
  154.                     break;
  155.                 default:
  156.                     $action 'append' $action;
  157.                     break;
  158.             }
  159.             $this->$action($spec$type$attrs);
  160.         }
  161.         return $this;
  162.     }
  163.     /**
  164.      * Start capture action
  165.      *
  166.      * @param string $captureType
  167.      * @param string $type
  168.      * @param array $attrs
  169.      *
  170.      * @return void
  171.      */
  172.     public function captureStart($captureType Container::APPEND$type 'text/javascript'$attrs = [])
  173.     {
  174.         if ($this->_captureLock) {
  175.             throw new Exception('Cannot nest headScript captures');
  176.         }
  177.         $this->_captureLock true;
  178.         $this->_captureType $captureType;
  179.         $this->_captureScriptType $type;
  180.         $this->_captureScriptAttrs $attrs;
  181.         ob_start();
  182.     }
  183.     /**
  184.      * End capture action and store
  185.      *
  186.      * @return void
  187.      */
  188.     public function captureEnd()
  189.     {
  190.         $content ob_get_clean();
  191.         $type $this->_captureScriptType;
  192.         $attrs $this->_captureScriptAttrs;
  193.         $this->_captureScriptType null;
  194.         $this->_captureScriptAttrs null;
  195.         $this->_captureLock false;
  196.         switch ($this->_captureType) {
  197.             case Container::SET:
  198.             case Container::PREPEND:
  199.             case Container::APPEND:
  200.                 $action strtolower($this->_captureType) . 'Script';
  201.                 break;
  202.             default:
  203.                 $action 'appendScript';
  204.                 break;
  205.         }
  206.         $this->$action($content$type$attrs);
  207.     }
  208.     /**
  209.      * Overload method access
  210.      *
  211.      * Allows the following method calls:
  212.      * - appendFile($src, $type = 'text/javascript', $attrs = array())
  213.      * - offsetSetFile($index, $src, $type = 'text/javascript', $attrs = array())
  214.      * - prependFile($src, $type = 'text/javascript', $attrs = array())
  215.      * - setFile($src, $type = 'text/javascript', $attrs = array())
  216.      * - appendScript($script, $type = 'text/javascript', $attrs = array())
  217.      * - offsetSetScript($index, $src, $type = 'text/javascript', $attrs = array())
  218.      * - prependScript($script, $type = 'text/javascript', $attrs = array())
  219.      * - setScript($script, $type = 'text/javascript', $attrs = array())
  220.      *
  221.      * @param  string $method
  222.      * @param  array $args
  223.      *
  224.      * @return HeadScript
  225.      *
  226.      * @throws Exception if too few arguments or invalid method
  227.      */
  228.     public function __call($method$args)
  229.     {
  230.         if (preg_match('/^(?P<action>set|(ap|pre)pend|offsetSet)(?P<mode>File|Script)$/'$method$matches)) {
  231.             if (count($args)) {
  232.                 throw new Exception(sprintf('Method "%s" requires at least one argument'$method));
  233.             }
  234.             $action $matches['action'];
  235.             $mode strtolower($matches['mode']);
  236.             $type 'text/javascript';
  237.             $attrs = [];
  238.             $index null;
  239.             if ('offsetSet' == $action) {
  240.                 $index array_shift($args);
  241.                 if (count($args)) {
  242.                     throw new Exception(sprintf('Method "%s" requires at least two arguments, an index and source'$method));
  243.                 }
  244.             }
  245.             $content $args[0];
  246.             if (isset($args[1])) {
  247.                 $type = (string) $args[1];
  248.             }
  249.             if (isset($args[2])) {
  250.                 $attrs = (array) $args[2];
  251.             }
  252.             switch ($mode) {
  253.                 case 'script':
  254.                     $item $this->createData($type$attrs$content);
  255.                     if ('offsetSet' == $action) {
  256.                         $this->offsetSet($index$item);
  257.                     } else {
  258.                         $this->$action($item);
  259.                     }
  260.                     break;
  261.                 case 'file':
  262.                 default:
  263.                     if (!$this->_isDuplicate($content) || $action == 'set') {
  264.                         $attrs['src'] = $content;
  265.                         $item $this->createData($type$attrs);
  266.                         if ('offsetSet' == $action) {
  267.                             $this->offsetSet($index$item);
  268.                         } else {
  269.                             $this->$action($item);
  270.                         }
  271.                     }
  272.                     break;
  273.             }
  274.             return $this;
  275.         }
  276.         return parent::__call($method$args);
  277.     }
  278.     /**
  279.      * Is the file specified a duplicate?
  280.      *
  281.      * @param  string $file
  282.      *
  283.      * @return bool
  284.      */
  285.     protected function _isDuplicate($file)
  286.     {
  287.         foreach ($this->getContainer() as $item) {
  288.             if (($item->source === null)
  289.                 && array_key_exists('src'$item->attributes)
  290.                 && ($file == $item->attributes['src'])) {
  291.                 return true;
  292.             }
  293.         }
  294.         return false;
  295.     }
  296.     /**
  297.      * Is the script provided valid?
  298.      *
  299.      * @param mixed $value
  300.      *
  301.      * @return bool
  302.      */
  303.     protected function _isValid($value)
  304.     {
  305.         if ((!$value instanceof \stdClass)
  306.             || !isset($value->type)
  307.             || (!isset($value->source) && !isset($value->attributes))) {
  308.             return false;
  309.         }
  310.         return true;
  311.     }
  312.     /**
  313.      * Override append
  314.      *
  315.      * @param  string $value
  316.      *
  317.      * @return void
  318.      */
  319.     public function append($value)
  320.     {
  321.         if (!$this->_isValid($value)) {
  322.             throw new Exception('Invalid argument passed to append(); please use one of the helper methods, appendScript() or appendFile()');
  323.         }
  324.         $this->getContainer()->append($value);
  325.     }
  326.     /**
  327.      * Override prepend
  328.      *
  329.      * @param  string $value
  330.      *
  331.      * @return void
  332.      */
  333.     public function prepend($value)
  334.     {
  335.         if (!$this->_isValid($value)) {
  336.             throw new Exception('Invalid argument passed to prepend(); please use one of the helper methods, prependScript() or prependFile()');
  337.         }
  338.         $this->getContainer()->prepend($value);
  339.     }
  340.     /**
  341.      * Override set
  342.      *
  343.      * @param  string $value
  344.      *
  345.      * @return void
  346.      */
  347.     public function set($value)
  348.     {
  349.         if (!$this->_isValid($value)) {
  350.             throw new Exception('Invalid argument passed to set(); please use one of the helper methods, setScript() or setFile()');
  351.         }
  352.         $this->getContainer()->set($value);
  353.     }
  354.     /**
  355.      * Override offsetSet
  356.      *
  357.      * @param  string|int $index
  358.      * @param  mixed $value
  359.      *
  360.      * @return void
  361.      */
  362.     public function offsetSet($index$value)
  363.     {
  364.         if (!$this->_isValid($value)) {
  365.             throw new Exception('Invalid argument passed to offsetSet(); please use one of the helper methods, offsetSetScript() or offsetSetFile()');
  366.         }
  367.         $this->getContainer()->offsetSet($index$value);
  368.     }
  369.     /**
  370.      * Set flag indicating if arbitrary attributes are allowed
  371.      *
  372.      * @param  bool $flag
  373.      *
  374.      * @return HeadScript
  375.      */
  376.     public function setAllowArbitraryAttributes($flag)
  377.     {
  378.         $this->_arbitraryAttributes = (bool) $flag;
  379.         return $this;
  380.     }
  381.     /**
  382.      * Are arbitrary attributes allowed?
  383.      *
  384.      * @return bool
  385.      */
  386.     public function arbitraryAttributesAllowed()
  387.     {
  388.         return $this->_arbitraryAttributes;
  389.     }
  390.     /**
  391.      * Create script HTML
  392.      *
  393.      * @param \stdClass $item
  394.      * @param string $indent
  395.      * @param string $escapeStart
  396.      * @param string $escapeEnd
  397.      *
  398.      * @return string
  399.      */
  400.     public function itemToString($item$indent$escapeStart$escapeEnd)
  401.     {
  402.         $attrString '';
  403.         $type = ($this->_autoEscape) ? $this->_escape($item->type) : $item->type;
  404.         if ($type != 'text/javascript') {
  405.             $item->attributes['type'] = $type;
  406.         }
  407.         if (!empty($item->attributes)) {
  408.             foreach ($item->attributes as $key => $value) {
  409.                 if ((!$this->arbitraryAttributesAllowed() && !in_array($key$this->_optionalAttributes))
  410.                     || in_array($key, ['conditional''noescape'])) {
  411.                     continue;
  412.                 }
  413.                 if ('defer' == $key) {
  414.                     $value 'defer';
  415.                 }
  416.                 $attrString .= sprintf(' %s="%s"'$key, ($this->_autoEscape) ? $this->_escape($value) : $value);
  417.             }
  418.         }
  419.         $addScriptEscape = !(isset($item->attributes['noescape']) && filter_var($item->attributes['noescape'], FILTER_VALIDATE_BOOLEAN));
  420.         $html '<script' $attrString '>';
  421.         if (!empty($item->source)) {
  422.             $html .= PHP_EOL ;
  423.             if ($addScriptEscape) {
  424.                 $html .= $indent '    ' $escapeStart PHP_EOL;
  425.             }
  426.             $html .= $indent '    ' $item->source;
  427.             if ($addScriptEscape) {
  428.                 $html .= $indent '    ' $escapeEnd PHP_EOL;
  429.             }
  430.             $html .= $indent;
  431.         }
  432.         $html .= '</script>';
  433.         if (isset($item->attributes['conditional'])
  434.             && !empty($item->attributes['conditional'])
  435.             && is_string($item->attributes['conditional'])) {
  436.             // inner wrap with comment end and start if !IE
  437.             if (str_replace(' '''$item->attributes['conditional']) === '!IE') {
  438.                 $html '<!-->' $html '<!--';
  439.             }
  440.             $html $indent '<!--[if ' $item->attributes['conditional'] . ']>' $html '<![endif]-->';
  441.         } else {
  442.             $html $indent $html;
  443.         }
  444.         return $html;
  445.     }
  446.     /**
  447.      * Retrieve string representation
  448.      *
  449.      * @param  string|int $indent
  450.      *
  451.      * @return string
  452.      */
  453.     public function toString($indent null)
  454.     {
  455.         $this->prepareEntries();
  456.         $indent = (null !== $indent)
  457.             ? $this->getWhitespace($indent)
  458.             : $this->getIndent();
  459.         $useCdata $this->useCdata true false;
  460.         $escapeStart = ($useCdata) ? '//<![CDATA[' '//<!--';
  461.         $escapeEnd = ($useCdata) ? '//]]>' '//-->';
  462.         $items = [];
  463.         $this->getContainer()->ksort();
  464.         foreach ($this as $item) {
  465.             if (!$this->_isValid($item)) {
  466.                 continue;
  467.             }
  468.             $items[] = $this->itemToString($item$indent$escapeStart$escapeEnd);
  469.         }
  470.         $return implode($this->getSeparator(), $items);
  471.         return $return;
  472.     }
  473.     protected function prepareEntries()
  474.     {
  475.         foreach ($this as &$item) {
  476.             if (!$this->_isValid($item)) {
  477.                 continue;
  478.             }
  479.             if ($this->isCacheBuster()) {
  480.                 // adds the automatic cache buster functionality
  481.                 if (is_array($item->attributes)) {
  482.                     if (isset($item->attributes['src'])) {
  483.                         $realFile PIMCORE_WEB_ROOT $item->attributes['src'];
  484.                         if (file_exists($realFile)) {
  485.                             $item->attributes['src'] = '/cache-buster-' filemtime($realFile) . $item->attributes['src'];
  486.                         }
  487.                     }
  488.                 }
  489.             }
  490.             $event = new GenericEvent($this, [
  491.                 'item' => $item,
  492.             ]);
  493.             \Pimcore::getEventDispatcher()->dispatch(FrontendEvents::VIEW_HELPER_HEAD_SCRIPT$event);
  494.             if (isset($item->attributes) && is_array($item->attributes)) {
  495.                 $source = (string)($item->attributes['src'] ?? '');
  496.                 $itemAttributes $item->attributes;
  497.                 if (isset($item->attributes['webLink'])) {
  498.                     unset($item->attributes['webLink']);
  499.                 }
  500.                 if (!empty($source)) {
  501.                     $this->handleWebLink($item$source$itemAttributes);
  502.                 }
  503.             }
  504.         }
  505.     }
  506.     /**
  507.      * Create data item containing all necessary components of script
  508.      *
  509.      * @param  string $type
  510.      * @param  array $attributes
  511.      * @param  string $content
  512.      *
  513.      * @return \stdClass
  514.      */
  515.     public function createData($type, array $attributes$content null)
  516.     {
  517.         $data = new \stdClass();
  518.         $data->type $type;
  519.         $data->attributes $attributes;
  520.         $data->source $content;
  521.         return $data;
  522.     }
  523. }
  524. class_alias(HeadScript::class, 'Pimcore\Templating\Helper\HeadScript');