Skip to content

Commit

Permalink
Implement better CSS parsing to fix relative links.
Browse files Browse the repository at this point in the history
  • Loading branch information
Cody Lundquist committed Mar 18, 2015
1 parent 9d92617 commit 5af3a90
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 92 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
9 changes: 7 additions & 2 deletions config/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
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')) {
mb_internal_encoding(MUNEE_CHARACTER_ENCODING);
}

/**
* 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);
Expand All @@ -39,4 +44,4 @@
*/
Registry::register(array('jpg', 'jpeg', 'gif', 'png'), function (\Munee\Request $Request) {
return new \Munee\Asset\Type\Image($Request);
});
});
10 changes: 7 additions & 3 deletions src/Munee/Asset/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,17 +301,21 @@ 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);

Utils::createDir($cacheDir);

return $cacheDir . DS . substr($fileHash, 2) . '-' . $optionsHash . '.' . $ext;
}
}
}
170 changes: 85 additions & 85 deletions src/Munee/Asset/Type/Css.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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));
}
}

Expand Down Expand Up @@ -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 <strong>' . $originalFile .
'</strong>. The following URL goes above webroot: <strong>' . $filePath .
'</strong>'
);
}
$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 <strong>' . $originalFile .
'</strong>. The following URL goes above webroot: <strong>' . $url . '</strong>'
);
}

$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;
}
}
2 changes: 1 addition & 1 deletion tests/Munee/Cases/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,4 @@ protected function getHeaders()

return $ret;
}
}
}

0 comments on commit 5af3a90

Please sign in to comment.