turquoiseowl / i18n

Smart internationalization for ASP.NET
Other
556 stars 156 forks source link

Relative URLs for images, css, JavaScript #286

Closed amainiii closed 7 years ago

amainiii commented 7 years ago

When resources (image, css, javascript) are embedded using relative paths, the browser will request them with the language tag. Because these resources would typically be excluded due to filtering, the language tag would not be removed from the URL. The only solution we can find to handle relative paths to resources is to set the QuickUrlExclusionFilter to null.

Because the application might not be installed directly underneath a host name, recommended best practice for ASP.NET is to use "~/images/resource.png" or "images/resource.png" rather than "/images/resource.png".

If I'm understanding this correctly, the QuickUrlExclusionFilter would not be useful in these cases and perhaps a caveat should be added to the manual/wiki to help future adopters.

turquoiseowl commented 7 years ago

Hi, I'm not totally with you. Would you try explaining again.

On the point about ~/images/resource.png: doesn't that get resolved to an absolute URL or can it still be resolved to a relative one? But anyhow i18n should work with relative paths okay.

turquoiseowl commented 7 years ago

BTW, it really helps me to talk in terms of HTTP and requests/responses. Thanks.

amainiii commented 7 years ago

On the point about ~/images/resource.png: doesn't that get resolved to an absolute URL or can it still be resolved to a relative one? But anyhow i18n should work with relative paths okay.

ASP.NET converts ~/images/resource.png to a relative path: "images/resource.png". The browser thinks that the web page that contains the reference to the image is (for example) "http://host.domain.tld/en/Default.aspx". So when the browser does a GET on the image, it requests "http://host.domain.tld/en/images/resource.png". Because the QuickUrlExclusionFilter bypasses these image files, the language tag (en) is not removed from the path, and the result is 404, not found.

I think ASP.NET used to convert tilde paths to absolute URLs prior to .NET 4.0, but I can't swear to it. I'll provide a sample ASP.NET solution for Visual Studio 2015, if that helps.

turquoiseowl commented 7 years ago

Checking a site here (ASP.NET v4.0), for this markup (in Razor):

<img src="~/Content/img/dslogo-full-inverse-32.png" />

I get in the response:

<img src="/Content/img/dslogo-full-inverse-32.png"  />

(Note the leading slash.)

So your leading slash is being dropped? I.e. "~/" goes to "" and not "/"?

turquoiseowl commented 7 years ago

It's hard to find any hard and fast documentation on what the tilde means, but the best I can find is that it represents the application root, in which case I'm struggling to see how/why the original slash is being dropped from "images". What is your version of ASP.NET and IIS? This must be something new in those versions.

amainiii commented 7 years ago

Our current theory is that MVC is converting "~" paths to "rooted" paths, but Web Forms is converting everything to relative paths. The corresponding .NET method calls are ResolveUrl (which "roots" everything) and ResolveClientUrl which generates relative paths. We've been looking at this all morning, and would like to debug the i18n in visual studio, but have never implemented/tested a System.Web.IHttpModule, so are not sure exactly how to proceed to debug.

amainiii commented 7 years ago

Attached is a Visual Studio 2015 ASP.NET 4.5 Web Forms solution that illustrates the problems we are having with relative URLs. Samplei18n.zip

turquoiseowl commented 7 years ago

If it helps, some notes here on how to debug i18n: https://github.com/turquoiseowl/i18n/issues/109

turquoiseowl commented 7 years ago

Here's a hack on the web:

<a href="<%= Page.ResolveUrl("~/contactus.aspx")%>" >Contact us</a>
amainiii commented 7 years ago

We have added/modified the following code near the end of LocalizeUrl in i18n/Concrete/EarlyUrlLocalizer.cs and it seems to resolve our issues with ASP.NET web forms (at least so far). Not sure if this has an adverse impact on MVC so we don't feel comfortable forking/pushing/pulling yet.

// root the url relative to the application root, eliminating "../..", "./", etc.
Uri newUri = new Uri(requestUrl, url);
String newUrl = newUri.PathAndQuery;

// Localize the URL.
return m_urlLocalizer.SetLangTagInUrlPath(context, newUrl, 
     UriKind.RelativeOrAbsolute, langtag);
turquoiseowl commented 7 years ago

Yeah, that looks good. I've tweaked slightly in the hope of making it optimally efficient. Let me know if it still works or not. If it does I can patch it in:

            // If url is un-rooted...make it absolute based on the current request URL.
            // By un-rooted we mean it is not absolute (i.e. it starts from the path component),
            // and that that path component itself is a relative path (i.e. it doesn't start with a /).
            // In doing so we also eliminate any "../..", "./", etc.
            // Examples:
            //      url (before)                            requestUrl                  url (after)
            //      -------------------------------------------------------------------------------------------------------------------
            //      content/fred.jpg                        http://example.com/blog     /blog/content/fred.jpg              (changed)
            //      /content/fred.jpg                       http://example.com/blog     /content/fred.jpg                   (unchanged)
            //      http://example.com/content/fred.jpg     http://example.com/blog     http://example.com/content/fred.jpg (unchanged)
            //
            if (!url.StartsWith("/")
                && !Uri.IsWellFormedUriString(url, UriKind.Absolute)) {
                Uri newUri = new Uri(requestUrl, url);
                url = newUri.PathAndQuery;
            }

            // Localize the URL.
            return m_urlLocalizer.SetLangTagInUrlPath(context, url, UriKind.RelativeOrAbsolute, langtag);
amainiii commented 7 years ago

We've tested your changes and they work fine. We had some concerns about the performance of the Uri.IsWellFormedUriString method http://referencesource.microsoft.com/#System/net/System/UriExt.cs so we tried performance testing with several other alternatives, but none were clearly superior. So, if you wouldn't mind making the change next time its convenient, we'd appreciate it. - Thanks!

turquoiseowl commented 7 years ago

Good discovery about Uri.IsWellFormedUriString! I should have suspected. Anyhow it occurred to me we only need call it if the url doesn't contain a colon (in which case maybe we don't need to call it at all -- bit late for me to work that through).

Anyhow, you may want to check the commit. I've added some test cases as well so hopefully it's cracked.