vendor/contao/core-bundle/src/Resources/contao/library/Contao/Combiner.php line 273

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Contao.
  4.  *
  5.  * (c) Leo Feyer
  6.  *
  7.  * @license LGPL-3.0-or-later
  8.  */
  9. namespace Contao;
  10. use ScssPhp\ScssPhp\Compiler;
  11. use ScssPhp\ScssPhp\OutputStyle;
  12. /**
  13.  * Combines .css or .js files into one single file
  14.  *
  15.  * Usage:
  16.  *
  17.  *     $combiner = new Combiner();
  18.  *
  19.  *     $combiner->add('css/style.css');
  20.  *     $combiner->add('css/fonts.scss');
  21.  *     $combiner->add('css/print.less');
  22.  *
  23.  *     echo $combiner->getCombinedFile();
  24.  *
  25.  * @author Leo Feyer <https://github.com/leofeyer>
  26.  */
  27. class Combiner extends System
  28. {
  29.     /**
  30.      * The .css file extension
  31.      */
  32.     const CSS '.css';
  33.     /**
  34.      * The .js file extension
  35.      */
  36.     const JS '.js';
  37.     /**
  38.      * The .scss file extension
  39.      */
  40.     const SCSS '.scss';
  41.     /**
  42.      * The .less file extension
  43.      */
  44.     const LESS '.less';
  45.     /**
  46.      * Unique file key
  47.      * @var string
  48.      */
  49.     protected $strKey '';
  50.     /**
  51.      * Operation mode
  52.      * @var string
  53.      */
  54.     protected $strMode;
  55.     /**
  56.      * Files
  57.      * @var array
  58.      */
  59.     protected $arrFiles = array();
  60.     /**
  61.      * Root dir
  62.      * @var string
  63.      */
  64.     protected $strRootDir;
  65.     /**
  66.      * Web dir relative to $this->strRootDir
  67.      * @var string
  68.      */
  69.     protected $strWebDir;
  70.     /**
  71.      * Public constructor required
  72.      */
  73.     public function __construct()
  74.     {
  75.         $container System::getContainer();
  76.         $this->strRootDir $container->getParameter('kernel.project_dir');
  77.         $this->strWebDir StringUtil::stripRootDir($container->getParameter('contao.web_dir'));
  78.     }
  79.     /**
  80.      * Add a file to the combined file
  81.      *
  82.      * @param string $strFile    The file to be added
  83.      * @param string $strVersion An optional version number
  84.      * @param string $strMedia   The media type of the file (.css only)
  85.      *
  86.      * @throws \InvalidArgumentException If $strFile is invalid
  87.      * @throws \LogicException           If different file types are mixed
  88.      */
  89.     public function add($strFile$strVersion=null$strMedia='all')
  90.     {
  91.         $strType strrchr($strFile'.');
  92.         // Check the file type
  93.         if ($strType != self::CSS && $strType != self::JS && $strType != self::SCSS && $strType != self::LESS)
  94.         {
  95.             throw new \InvalidArgumentException("Invalid file $strFile");
  96.         }
  97.         $strMode = ($strType == self::JS) ? self::JS self::CSS;
  98.         // Set the operation mode
  99.         if ($this->strMode === null)
  100.         {
  101.             $this->strMode $strMode;
  102.         }
  103.         elseif ($this->strMode != $strMode)
  104.         {
  105.             throw new \LogicException('You cannot mix different file types. Create another Combiner object instead.');
  106.         }
  107.         // Check the source file
  108.         if (!file_exists($this->strRootDir '/' $strFile))
  109.         {
  110.             // Handle public bundle resources in the contao.web_dir folder
  111.             if (file_exists($this->strRootDir '/' $this->strWebDir '/' $strFile))
  112.             {
  113.                 $strFile $this->strWebDir '/' $strFile;
  114.             }
  115.             else
  116.             {
  117.                 return;
  118.             }
  119.         }
  120.         // Prevent duplicates
  121.         if (isset($this->arrFiles[$strFile]))
  122.         {
  123.             return;
  124.         }
  125.         // Default version
  126.         if ($strVersion === null)
  127.         {
  128.             $strVersion filemtime($this->strRootDir '/' $strFile);
  129.         }
  130.         // Store the file
  131.         $arrFile = array
  132.         (
  133.             'name' => $strFile,
  134.             'version' => $strVersion,
  135.             'media' => $strMedia,
  136.             'extension' => $strType
  137.         );
  138.         $this->arrFiles[$strFile] = $arrFile;
  139.         $this->strKey .= '-f' $strFile '-v' $strVersion '-m' $strMedia;
  140.     }
  141.     /**
  142.      * Add multiple files from an array
  143.      *
  144.      * @param array  $arrFiles   An array of files to be added
  145.      * @param string $strVersion An optional version number
  146.      * @param string $strMedia   The media type of the file (.css only)
  147.      */
  148.     public function addMultiple(array $arrFiles$strVersion=null$strMedia='screen')
  149.     {
  150.         foreach ($arrFiles as $strFile)
  151.         {
  152.             $this->add($strFile$strVersion$strMedia);
  153.         }
  154.     }
  155.     /**
  156.      * Check whether files have been added
  157.      *
  158.      * @return boolean True if there are files
  159.      */
  160.     public function hasEntries()
  161.     {
  162.         return !empty($this->arrFiles);
  163.     }
  164.     /**
  165.      * Generates the files and returns the URLs.
  166.      *
  167.      * @param string $strUrl An optional URL to prepend
  168.      *
  169.      * @return array The file URLs
  170.      */
  171.     public function getFileUrls($strUrl=null)
  172.     {
  173.         if ($strUrl === null)
  174.         {
  175.             $strUrl System::getContainer()->get('contao.assets.assets_context')->getStaticUrl();
  176.         }
  177.         $return = array();
  178.         $strTarget substr($this->strMode1);
  179.         $blnDebug System::getContainer()->getParameter('kernel.debug');
  180.         foreach ($this->arrFiles as $arrFile)
  181.         {
  182.             // Compile SCSS/LESS files into temporary files
  183.             if ($arrFile['extension'] == self::SCSS || $arrFile['extension'] == self::LESS)
  184.             {
  185.                 $strPath 'assets/' $strTarget '/' str_replace('/''_'$arrFile['name']) . $this->strMode;
  186.                 if ($blnDebug || !file_exists($this->strRootDir '/' $strPath))
  187.                 {
  188.                     $objFile = new File($strPath);
  189.                     $objFile->write($this->handleScssLess(file_get_contents($this->strRootDir '/' $arrFile['name']), $arrFile));
  190.                     $objFile->close();
  191.                 }
  192.                 $return[] = $strUrl $strPath '|' $arrFile['version'];
  193.             }
  194.             else
  195.             {
  196.                 $name $arrFile['name'];
  197.                 // Strip the contao.web_dir directory prefix (see #328)
  198.                 if (strncmp($name$this->strWebDir '/', \strlen($this->strWebDir) + 1) === 0)
  199.                 {
  200.                     $name substr($name, \strlen($this->strWebDir) + 1);
  201.                 }
  202.                 // Add the media query (see #7070)
  203.                 if ($this->strMode == self::CSS && $arrFile['media'] && $arrFile['media'] != 'all' && !$this->hasMediaTag($arrFile['name']))
  204.                 {
  205.                     $name .= '|' $arrFile['media'];
  206.                 }
  207.                 $return[] = $strUrl $name '|' $arrFile['version'];
  208.             }
  209.         }
  210.         return $return;
  211.     }
  212.     /**
  213.      * Generate the combined file and return its path
  214.      *
  215.      * @param string $strUrl An optional URL to prepend
  216.      *
  217.      * @return string The path to the combined file
  218.      */
  219.     public function getCombinedFile($strUrl=null)
  220.     {
  221.         if (System::getContainer()->getParameter('kernel.debug'))
  222.         {
  223.             return $this->getDebugMarkup($strUrl);
  224.         }
  225.         return $this->getCombinedFileUrl($strUrl);
  226.     }
  227.     /**
  228.      * Generates the debug markup.
  229.      *
  230.      * @param string $strUrl An optional URL to prepend
  231.      *
  232.      * @return string The debug markup
  233.      */
  234.     protected function getDebugMarkup($strUrl)
  235.     {
  236.         $return $this->getFileUrls($strUrl);
  237.         foreach ($return as $k=>$v)
  238.         {
  239.             $options StringUtil::resolveFlaggedUrl($v);
  240.             $return[$k] = $v;
  241.             if ($options->mtime)
  242.             {
  243.                 $return[$k] .= '?v=' substr(md5($options->mtime), 08);
  244.             }
  245.             if ($options->media)
  246.             {
  247.                 $return[$k] .= '" media="' $options->media;
  248.             }
  249.         }
  250.         if ($this->strMode == self::JS)
  251.         {
  252.             return implode('"></script><script src="'$return);
  253.         }
  254.         return implode('"><link rel="stylesheet" href="'$return);
  255.     }
  256.     /**
  257.      * Generate the combined file and return its path
  258.      *
  259.      * @param string $strUrl An optional URL to prepend
  260.      *
  261.      * @return string The path to the combined file
  262.      */
  263.     protected function getCombinedFileUrl($strUrl=null)
  264.     {
  265.         if ($strUrl === null)
  266.         {
  267.             $strUrl System::getContainer()->get('contao.assets.assets_context')->getStaticUrl();
  268.         }
  269.         $arrPrefix = array();
  270.         $strTarget substr($this->strMode1);
  271.         foreach ($this->arrFiles as $arrFile)
  272.         {
  273.             $arrPrefix[] = basename($arrFile['name']);
  274.         }
  275.         $strKey StringUtil::substr(implode(','$arrPrefix), 64'...') . '-' substr(md5($this->strKey), 08);
  276.         // Load the existing file
  277.         if (file_exists($this->strRootDir '/assets/' $strTarget '/' $strKey $this->strMode))
  278.         {
  279.             return $strUrl 'assets/' $strTarget '/' $strKey $this->strMode;
  280.         }
  281.         // Create the file
  282.         $objFile = new File('assets/' $strTarget '/' $strKey $this->strMode);
  283.         $objFile->truncate();
  284.         foreach ($this->arrFiles as $arrFile)
  285.         {
  286.             $content file_get_contents($this->strRootDir '/' $arrFile['name']);
  287.             // Remove UTF-8 BOM
  288.             if (strncmp($content"\xEF\xBB\xBF"3) === 0)
  289.             {
  290.                 $content substr($content3);
  291.             }
  292.             // HOOK: modify the file content
  293.             if (isset($GLOBALS['TL_HOOKS']['getCombinedFile']) && \is_array($GLOBALS['TL_HOOKS']['getCombinedFile']))
  294.             {
  295.                 foreach ($GLOBALS['TL_HOOKS']['getCombinedFile'] as $callback)
  296.                 {
  297.                     $this->import($callback[0]);
  298.                     $content $this->{$callback[0]}->{$callback[1]}($content$strKey$this->strMode$arrFile);
  299.                 }
  300.             }
  301.             if ($arrFile['extension'] == self::CSS)
  302.             {
  303.                 $content $this->handleCss($content$arrFile);
  304.             }
  305.             elseif ($arrFile['extension'] == self::SCSS || $arrFile['extension'] == self::LESS)
  306.             {
  307.                 $content $this->handleScssLess($content$arrFile);
  308.             }
  309.             $objFile->append($content);
  310.         }
  311.         unset($content);
  312.         $objFile->close();
  313.         return $strUrl 'assets/' $strTarget '/' $strKey $this->strMode;
  314.     }
  315.     /**
  316.      * Handle CSS files
  317.      *
  318.      * @param string $content The file content
  319.      * @param array  $arrFile The file array
  320.      *
  321.      * @return string The modified file content
  322.      */
  323.     protected function handleCss($content$arrFile)
  324.     {
  325.         $content $this->fixPaths($content$arrFile);
  326.         // Add the media type if there is no @media command in the code
  327.         if ($arrFile['media'] && $arrFile['media'] != 'all' && strpos($content'@media') === false)
  328.         {
  329.             $content '@media ' $arrFile['media'] . "{\n" $content "\n}";
  330.         }
  331.         return $content;
  332.     }
  333.     /**
  334.      * Handle SCSS/LESS files
  335.      *
  336.      * @param string $content The file content
  337.      * @param array  $arrFile The file array
  338.      *
  339.      * @return string The modified file content
  340.      */
  341.     protected function handleScssLess($content$arrFile)
  342.     {
  343.         $blnDebug System::getContainer()->getParameter('kernel.debug');
  344.         if ($arrFile['extension'] == self::SCSS)
  345.         {
  346.             $objCompiler = new Compiler();
  347.             $objCompiler->setImportPaths($this->strRootDir '/' . \dirname($arrFile['name']));
  348.             $objCompiler->setOutputStyle(($blnDebug OutputStyle::EXPANDED OutputStyle::COMPRESSED));
  349.             if ($blnDebug)
  350.             {
  351.                 $objCompiler->setSourceMap(Compiler::SOURCE_MAP_INLINE);
  352.             }
  353.             return $this->fixPaths($objCompiler->compileString($content$this->strRootDir '/' $arrFile['name'])->getCss(), $arrFile);
  354.         }
  355.         $strPath = \dirname($arrFile['name']);
  356.         $arrOptions = array
  357.         (
  358.             'strictMath' => true,
  359.             'compress' => !$blnDebug,
  360.             'import_dirs' => array($this->strRootDir '/' $strPath => $strPath)
  361.         );
  362.         $objParser = new \Less_Parser();
  363.         $objParser->SetOptions($arrOptions);
  364.         $objParser->parse($content);
  365.         return $this->fixPaths($objParser->getCss(), $arrFile);
  366.     }
  367.     /**
  368.      * Fix the paths
  369.      *
  370.      * @param string $content The file content
  371.      * @param array  $arrFile The file array
  372.      *
  373.      * @return string The modified file content
  374.      */
  375.     protected function fixPaths($content$arrFile)
  376.     {
  377.         $strName $arrFile['name'];
  378.         // Strip the contao.web_dir directory prefix
  379.         if (strpos($strName$this->strWebDir '/') === 0)
  380.         {
  381.             $strName substr($strName, \strlen($this->strWebDir) + 1);
  382.         }
  383.         $strDirname = \dirname($strName);
  384.         $strGlue = ($strDirname != '.') ? $strDirname '/' '';
  385.         return preg_replace_callback(
  386.             '/url\(("[^"\n]+"|\'[^\'\n]+\'|[^"\'\s()]+)\)/',
  387.             static function ($matches) use ($strDirname$strGlue)
  388.             {
  389.                 $strData $matches[1];
  390.                 if ($strData[0] == '"' || $strData[0] == "'")
  391.                 {
  392.                     $strData substr($strData1, -1);
  393.                 }
  394.                 // Skip absolute links and embedded images (see #5082)
  395.                 if ($strData[0] == '/' || $strData[0] == '#' || strncmp($strData'data:'5) === || strncmp($strData'http://'7) === || strncmp($strData'https://'8) === || strncmp($strData'assets/css3pie/'15) === 0)
  396.                 {
  397.                     return $matches[0];
  398.                 }
  399.                 // Make the paths relative to the root (see #4161)
  400.                 if (strncmp($strData'../'3) !== 0)
  401.                 {
  402.                     $strData '../../' $strGlue $strData;
  403.                 }
  404.                 else
  405.                 {
  406.                     $dir $strDirname;
  407.                     // Remove relative paths
  408.                     while (strncmp($strData'../'3) === 0)
  409.                     {
  410.                         $dir = \dirname($dir);
  411.                         $strData substr($strData3);
  412.                     }
  413.                     $glue = ($dir != '.') ? $dir '/' '';
  414.                     $strData '../../' $glue $strData;
  415.                 }
  416.                 $strQuote '';
  417.                 if ($matches[1][0] == "'" || $matches[1][0] == '"')
  418.                 {
  419.                     $strQuote $matches[1][0];
  420.                 }
  421.                 if (preg_match('/[(),\s"\']/'$strData))
  422.                 {
  423.                     if ($matches[1][0] == "'")
  424.                     {
  425.                         $strData str_replace("'""\\'"$strData);
  426.                     }
  427.                     else
  428.                     {
  429.                         $strQuote '"';
  430.                         $strData str_replace('"''\"'$strData);
  431.                     }
  432.                 }
  433.                 return 'url(' $strQuote $strData $strQuote ')';
  434.             },
  435.             $content
  436.         );
  437.     }
  438.     /**
  439.      * Check if the file has a @media tag
  440.      *
  441.      * @param string $strFile
  442.      *
  443.      * @return boolean True if the file has a @media tag
  444.      */
  445.     protected function hasMediaTag($strFile)
  446.     {
  447.         $return false;
  448.         $fh fopen($this->strRootDir '/' $strFile'r');
  449.         while (($line fgets($fh)) !== false)
  450.         {
  451.             if (strpos($line'@media') !== false)
  452.             {
  453.                 $return true;
  454.                 break;
  455.             }
  456.         }
  457.         fclose($fh);
  458.         return $return;
  459.     }
  460. }
  461. class_alias(Combiner::class, 'Combiner');