From 5af3a900a348d7ddde7d46291bbd7bceb01b4d65 Mon Sep 17 00:00:00 2001 From: Cody Lundquist Date: Tue, 17 Mar 2015 20:46:32 -0700 Subject: [PATCH] Implement better CSS parsing to fix relative links. --- composer.json | 3 +- config/bootstrap.php | 9 +- src/Munee/Asset/Type.php | 10 +- src/Munee/Asset/Type/Css.php | 170 ++++++++++++++--------------- tests/Munee/Cases/ResponseTest.php | 2 +- 5 files changed, 102 insertions(+), 92 deletions(-) diff --git a/composer.json b/composer.json index dca7625..6bf1e42 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "imagine/imagine": "0.6.2", "coffeescript/coffeescript": "1.3.1", "meenie/javascript-packer": "1.1", - "tubalmartin/cssmin": "~2.4" + "tubalmartin/cssmin": "~2.4", + "sabberworm/php-css-parser": "~6.0" } } diff --git a/config/bootstrap.php b/config/bootstrap.php index e5c611a..6894b67 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -14,6 +14,11 @@ defined('MUNEE_CACHE') || define('MUNEE_CACHE', MUNEE_FOLDER . DS . 'cache'); // Define default character encoding defined('MUNEE_CHARACTER_ENCODING') || define('MUNEE_CHARACTER_ENCODING', 'UTF-8'); +// Are we using Munee with URL Rewrite (.htaccess file)? +$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; +defined('MUNEE_USING_URL_REWRITE') || define('MUNEE_USING_URL_REWRITE', strpos($requestUri, 'files=') === false); +// Munee dispatcher file if not using URL Rewrite +defined('MUNEE_DISPATCHER_FILE') || define('MUNEE_DISPATCHER_FILE', ! MUNEE_USING_URL_REWRITE ? $_SERVER['SCRIPT_NAME'] : ''); // If mbstring is installed, set the encoding default if (function_exists('mb_internal_encoding')) { @@ -21,7 +26,7 @@ } /** - * Register the CSS Asset Class with the extensions .css and .less + * Register the CSS Asset Class with the extensions .css, .less, and .scss */ Registry::register(array('css', 'less', 'scss'), function (\Munee\Request $Request) { return new \Munee\Asset\Type\Css($Request); @@ -39,4 +44,4 @@ */ Registry::register(array('jpg', 'jpeg', 'gif', 'png'), function (\Munee\Request $Request) { return new \Munee\Asset\Type\Image($Request); -}); \ No newline at end of file +}); diff --git a/src/Munee/Asset/Type.php b/src/Munee/Asset/Type.php index f9513a5..f8bf37c 100644 --- a/src/Munee/Asset/Type.php +++ b/src/Munee/Asset/Type.php @@ -301,12 +301,16 @@ protected function checkCache($originalFile, $cacheFile) */ protected function generateCacheFile($file) { - $requestOptions = serialize($this->request->options); + $cacheSalt = serialize(array( + $this->request->options, + MUNEE_USING_URL_REWRITE, + MUNEE_DISPATCHER_FILE + )); $params = serialize($this->request->params); $ext = pathinfo($file, PATHINFO_EXTENSION); $fileHash = md5($file); - $optionsHash = md5($params . $requestOptions); + $optionsHash = md5($params . $cacheSalt); $cacheDir = $this->cacheDir . DS . substr($fileHash, 0, 2); @@ -314,4 +318,4 @@ protected function generateCacheFile($file) return $cacheDir . DS . substr($fileHash, 2) . '-' . $optionsHash . '.' . $ext; } -} \ No newline at end of file +} diff --git a/src/Munee/Asset/Type/Css.php b/src/Munee/Asset/Type/Css.php index 3241f86..e60902f 100644 --- a/src/Munee/Asset/Type/Css.php +++ b/src/Munee/Asset/Type/Css.php @@ -12,6 +12,9 @@ use Munee\Asset\Type; use lessc; use Leafo\ScssPhp\Compiler as ScssCompiler; +use Sabberworm\CSS\Parser as CssParser; +use Sabberworm\CSS\Property\Import; +use Sabberworm\CSS\Value\URL; /** * Handles CSS @@ -87,7 +90,7 @@ protected function beforeFilter($originalFile, $cacheFile) } catch (\Exception $e) { throw new CompilationException('Error in LESS Compiler', 0, $e); } - $compiledLess['compiled'] = $this->fixRelativeImagePaths($compiledLess['compiled'], $originalFile); + $compiledLess['compiled'] = $this->fixRelativePaths($compiledLess['compiled'], $originalFile); file_put_contents($cacheFile, serialize($compiledLess)); } elseif ($this->isScss($originalFile)) { $scss = new ScssCompiler(); @@ -105,12 +108,11 @@ protected function beforeFilter($originalFile, $cacheFile) $content['files'][$file] = filemtime($file); } - $content['compiled'] = $this->fixRelativeImagePaths($content['compiled'], $originalFile); + $content['compiled'] = $this->fixRelativePaths($content['compiled'], $originalFile); file_put_contents($cacheFile, serialize($content)); } else { $content = file_get_contents($originalFile); - $content = self::parseImports($content,$originalFile); - file_put_contents($cacheFile, $this->fixRelativeImagePaths($content, $originalFile)); + file_put_contents($cacheFile, $this->fixRelativePaths($content, $originalFile)); } } @@ -157,107 +159,105 @@ protected function isScss($file) } /** - * Fixes relative paths to absolute paths + * Use CssParser to go through and convert all relative paths to absolute * - * @param $content - * @param $originalFile + * @param string $content + * @param string $originalFile * * @return string - * - * @throws CompilationException */ - protected function fixRelativeImagePaths($content, $originalFile) + protected function fixRelativePaths($content, $originalFile) { - $regEx = '%(url[\\s]*\\()(?!data:image)[\\s\'"]*([^\\)\'"]*)[\\s\'"]*(\\))%'; + $cssParser = new CssParser($content); + $cssDocument = $cssParser->parse(); - $webroot = $this->request->webroot; - $changedContent = preg_replace_callback($regEx, function ($match) use ($originalFile, $webroot) { - $filePath = trim($match[2]); - // Skip conversion if the first character is a '/' since it's already an absolute path - // Also skip conversion if the string has an protocol in url - if ($filePath[0] !== '/' && strpos($filePath, '://') === false) { - $basePath = SUB_FOLDER . str_replace($webroot, '', dirname($originalFile)); - $basePathParts = array_reverse(array_filter(explode('/', $basePath))); - $numOfRecursiveDirs = substr_count($filePath, '../'); - if ($numOfRecursiveDirs > count($basePathParts)) { - throw new CompilationException( - 'Error in stylesheet ' . $originalFile . - '. The following URL goes above webroot: ' . $filePath . - '' - ); - } + $cssBlocks = $cssDocument->getAllValues(); - $basePathParts = array_slice($basePathParts, $numOfRecursiveDirs); - $basePath = implode('/', array_reverse($basePathParts)); + $this->fixUrls($cssBlocks, $originalFile); - if (! empty($basePath) && $basePath[0] != '/') { - $basePath = '/' . $basePath; + return $cssDocument->render(); + } + + /** + * Recursively go through the CSS Blocks and update relative links to absolute + * + * @param $cssBlocks + * @param $originalFile + * @throws CompilationException + */ + protected function fixUrls($cssBlocks, $originalFile) { + foreach ($cssBlocks as $cssBlock) { + if ($cssBlock instanceof Import) { + $this->fixUrls($cssBlock->atRuleArgs(), $originalFile); + } else { + if (! $cssBlock instanceof URL) { + continue; } - $filePath = $basePath . '/' . $filePath; - $filePath = str_replace(array('../', './'), '', $filePath); + $originalUrl = $cssBlock->getURL()->getString(); + $url = $this->relativeToAbsolute($originalUrl, $originalFile); + $cssBlock->getURL()->setString($url); } - - return $match[1] . $filePath . $match[3]; - }, $content); - - if (null !== $changedContent) { - $content = $changedContent; } - - return $content; } /** - * Parses $origFile for @imports and reads the contents of the imported - * file(s) if possible. Does recursion to resolve @imports in imported - * files as well. Wraps imported contents into @media ... { ... } markup - * if needed. - * - * Example: - * - * @import url(reset.css) screen, projection; + * Convert the passed in url from relative to absolute taking care not to convert urls that are already + * absolute, point to a different domain/protocol, or are base64 encoded "data:image" strings. + * It will also prefix a url with the munee dispatcher file URL if *not* using URL Rewrites (.htaccess). * - * Result: - * - * @media screen, projection { ... } - * - * @access protected - * - * @param string $content - * @param string $origFile + * @param $originalUrl + * @param $originalFile * * @return string - **/ - protected function parseImports($content, $origFile) + * @throws CompilationException + */ + protected function relativeToAbsolute($originalUrl, $originalFile) { - $dir = dirname($origFile); - // matches any type of import rule - preg_match_all('~@import\s*(?:url)?(?:\(?\'?\"?)?([^\'\"\)\(]*)(?:\'?\"?)?\)?\s?([^;]*);~im', $content, $imports, PREG_SET_ORDER); - foreach($imports as $i => $item) { - $file = $dir.'/'.$item[1]; - $media = $item[2]; - if (is_file($file)) { - $string = file_get_contents($file); - $newDir = dirname($file); - // replace imports in current file - $string = $this->parseImports($string, $file); - // replace urls - if ($newDir !== $dir) { - $tmp = $dir.'/'; - if (substr($newDir, 0, strlen($tmp)) === $tmp) { - $string = preg_replace('#\burl\(["\']?(?=[.\w])(?!\w+:)#', '$0' . substr($newDir, strlen($tmp)) . '/', $string); - } - } - if (! empty($media)) { - $string = '@media '.trim($media).' {' - . $string - . '}'; - } - $content = str_replace($imports[$i][0],$string,$content); + $webroot = $this->request->webroot; + $url = $originalUrl; + if ( + $originalUrl[0] !== '/' && + strpos($originalUrl, '://') === false && + strpos($originalUrl, 'data:image') === false + ) { + $basePath = SUB_FOLDER . str_replace($webroot, '', dirname($originalFile)); + $basePathParts = array_reverse(array_filter(explode('/', $basePath))); + $numOfRecursiveDirs = substr_count($originalUrl, '../'); + if ($numOfRecursiveDirs > count($basePathParts)) { + throw new CompilationException( + 'Error in stylesheet ' . $originalFile . + '. The following URL goes above webroot: ' . $url . '' + ); } + + $basePathParts = array_slice($basePathParts, $numOfRecursiveDirs); + $basePath = implode('/', array_reverse($basePathParts)); + + if (! empty($basePath) && $basePath[0] != '/') { + $basePath = '/' . $basePath; + } + + $url = $basePath . '/' . $originalUrl; + $url = str_replace(array('../', './'), '', $url); } - return $content; - } + // If not using URL Rewrite + if (! MUNEE_USING_URL_REWRITE) { + $dispatcherUrl = MUNEE_DISPATCHER_FILE . '?files='; + // If url is not already pointing to munee dispatcher file, + // isn't pointing to another domain/protocol, + // and isn't using data:image + if ( + strpos($url, $dispatcherUrl) !== 0 && + strpos($originalUrl, '://') === false && + strpos($originalUrl, 'data:image') === false + ) { + $url = str_replace('?', '&', $url); + $url = $dispatcherUrl . $url; + } + } + + return $url; + } } diff --git a/tests/Munee/Cases/ResponseTest.php b/tests/Munee/Cases/ResponseTest.php index 69dbf41..8895b95 100644 --- a/tests/Munee/Cases/ResponseTest.php +++ b/tests/Munee/Cases/ResponseTest.php @@ -179,4 +179,4 @@ protected function getHeaders() return $ret; } -} \ No newline at end of file +}