1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
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)
- ...