public string Transform(string cssPath, string cssContent)
{
return Regex.Replace(cssContent, @"url\((?<url>.*?)\)",
match => FixUrl(cssPath, match),
RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
}
/// <summary>
/// Assume cssPath is /content/site.css:
/// * in: path/to/image.gif -> out: /content/path/to/image.gif
/// * in: ../path/to/image.gif -> out: /path/to/image.gif
/// * in: /path/to/image.gif -> out: /path/to/image.gif
/// </summary>
private static string FixUrl(string cssPath, Match match)
{
try
{
var url = match.Groups["url"].Value;
const string template = "url({0})";
if (url.StartsWith("/"))
return url;
var adjustedResourceFolder = cssPath.Substring(0, resourcePath.LastIndexOf("/"));
var backFolderCount = Regex.Matches(url, @"\.\./").Count;
for (int i = 0; i < backFolderCount; i++)
{
url = url.Substring(3);
adjustedResourceFolder = adjustedResourceFolder.Substring(0,
adjustedResourceFolder.LastIndexOf("/"));
}
return string.Format(template, (adjustedResourceFolder + "/" + url));
}
catch (Exception ex)
{
return match.Value;
}
}
Refactorings
No refactoring yet !
Ants
October 31, 2009, October 31, 2009 08:04, permalink
Looks like a bug in line 21 where it'll return "/path/to/image.gif" instead of "url(/path/to/image.gif)". Or is this intentional?
The logic in lines 22-29 doesn't seem like it'll handle the case when url = "path/to/../to/image.gif". Or is this kind of input illegal?
Buu Nguyen
October 31, 2009, October 31, 2009 09:52, permalink
@Ants: Thanks. You're right about the bug in line 21. Re. 22-29, I don't think "path/to/../to/image.gif" is a valid CSS URL. How do you come up with that in the first place :)?
The newly refactored code is below.
private static string FixUrl(string cssPath, Match match)
{
try
{
const string template = "url(\"{0}\")";
var url = match.Groups["url"].Value.Trim('\"', '\'');
if (url.StartsWith("/"))
return string.Format(template, url);
var cssFolder = cssPath.Substring(0, cssPath.LastIndexOf("/"));
var backFolderCount = Regex.Matches(url, @"\.\./").Count;
for (int i = 0; i < backFolderCount; i++)
{
url = url.Substring(3); // skip 1 '../'
cssFolder = cssFolder.Substring(0, cssFolder.LastIndexOf("/")); // move back 1 folder
}
return string.Format(template, cssFolder + "/" + url);
}
catch (Exception ex)
{
return match.Value;
}
}
Ants
October 31, 2009, October 31, 2009 18:53, permalink
@buu: I came up with "path/to/../to/image.gif" from experience because this is how people used to hack older versions of IIS by having a path like: "images/../../windows/system32/notepad.exe". Newer versions of IIS won't let you move up past the root anymore.
I think that having the "../" embedded in the path is legal. I didn't see anything in the CSS grammar nor in HTTP RFC saying that if a URL is relative, all the "../" have to be at the beginning of the path. Do you have a reference that says this? I do know that often FireFox and IE can't agree on how to interpret a relative path, but often it's already broken even without the "../".
BTW, did you see this article about fixing up relative URL paths in CSS?
http://devtoolshed.com/content/fixing-relative-paths-c-aspnet-when-using-url-rewriting
Buu Nguyen
November 1, 2009, November 01, 2009 06:52, permalink
@Ants: that's an interesting observation. I am not aware of any source saying "../" is not allowed in the middle/end either. I'm thinking whether this is typical enough to tackle it in the code though.
Thanks for the link, it's a super cool technique.
Ants
November 1, 2009, November 01, 2009 11:54, permalink
This handles relative paths in both the cssPath as well as the url.
class PathBuilder
{
List<string> _segments = new List<string>();
public IEnumerable<string> Segments
{
get { return _segments.ToArray<string>(); }
}
public void Add(IEnumerable<string> segments)
{
foreach (string segment in segments)
Add(segment);
}
public void Add(string segment)
{
switch (segment)
{
case "":
case ".":
// Do nothing
break;
case "..":
RemoveEnd();
break;
default:
_segments.Add(segment);
break;
}
}
public void RemoveEnd()
{
if (_segments.Count > 0)
_segments.RemoveAt(_segments.Count - 1);
}
public override string ToString()
{
return "/" + String.Join("/", _segments.ToArray());
}
}
static IEnumerable<string> GetSegments(string path)
{
if (String.IsNullOrEmpty(path))
return new List<string>();
return path.Split('/');
}
static string FixUrl(IEnumerable<string> baseSegments, Match match)
{
var builder = new PathBuilder();
var url = match.Groups["url"].Value.Trim('\"', '\'');
if (!url.StartsWith("/"))
builder.Add(baseSegments);
builder.Add(GetSegments(url));
return string.Format("url(\"{0}\")", builder.ToString());
}
static IEnumerable<string> GetBaseSegments(string cssPath)
{
var baseBuilder = new PathBuilder();
baseBuilder.Add(GetSegments(cssPath));
baseBuilder.RemoveEnd();
return baseBuilder.Segments;
}
public static string Transform(string cssPath, string cssContent)
{
var baseSegments = GetBaseSegments(cssPath);
return Regex.Replace(cssContent,
@"url\((?<url>.*?)\)",
match => FixUrl(baseSegments, match),
RegexOptions.IgnoreCase
| RegexOptions.Singleline
| RegexOptions.ExplicitCapture);
}
This function changes all URLs inside a CSS file which are relative to the path of that CSS file into URLs which are relative to the web application. For instance, if the CSS file path is /content/site.css, then:
- Any occurence of url(path/to/image.gif) will become url(/content/path/to/image.gif)
- Any occurence of url(../path/to/image.gif) will become url(/path/to/image.gif)
- ...