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

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