Archives

Archives / 2007 / January
  • How to work around the access denied cross-domain frame issue in ASP.NET Ajax 1.0

    Some users have run into an issue when hosting ASP.NET Ajax applications in a frame or iframe that's in a different domain from the top-level window. If you try to do that and browse to the page using IE, you'll receive an "access denied" error client-side any time a DOM event is raised in the frame. The code that's responsible for that is Sys.UI.getLocation. This is a tricky piece of code that determines the pixel coordinates of a DOM element relative to the top-left corner of the page, so that an absolutely positioned child of the body using these coordinates would exactly cover the element you measured. This is useful for drag & drop, popup scenarios like auto-complete and when an event is raised to get mouse coordinates relative to the event source. This is this last piece that explains the problem. The code in this method is different for each of the browsers that we support because each one of them has its own behavior that cannot be determined as we usually do by dynamically looking at capabilities. You just have to know for example that browser X is not counting an element's scroll position if it's absolutely positioned and a direct child of body (names changed to protect the guilty). That's the kind of problem we had to work around. Luckily, IE has two very handy methods to retrieve this kind of coordinates that enables us to bypass completely a number of bugs that we just couldn't efficiently work around: getClientRects, which gets all rectangles the element occupies on the page, and getBoundingClientRect, which returns a single rectangle that bounds the whole element. In the method that we shipped, we've been using getClientRects and getting the first rectangle because we wanted to have consistent behavior across browsers even if the element is a wrapping element such as a span: in this case, the top-left corner of the element is the top-left corner of the first bounding rectangle, which is different from the top-left corner of the global bounding rectangle:

    And this is where we made a mistake, unfortunately too late. There is a subtle difference between getClientRects and getBoundingClientRect, which is that getClientRects, when in an iframe, gives coordinates that include the offset of that frame in the top window, whereas getBoundingClientRect gives the right coordinates directly. Both need to include the frameborder to be perfectly accurate. To correct the behavior of getClientRects, we had to look at the coordinates of the frame relative to the top window, to subtract them, and this is the operation that is not allowed if the frames are in different domains.

    The fix is to use getBoundingClientRect instead, which will introduce a small inconsistency across browsers in the case of wrapping elements but is a lot better than just failing. The new version of the function still needs to try/catch around the code that fixes the frameborder, so for cross-domain frames you may get a 2 pixel offset in the coordinates but this is the best you can get.

    How to apply the fix

    First, you’ll need to use the external script files instead of the resource-based ones. You do this by setting a general ScriptPath on the ScriptManager. The external script files can be found in the Microsoft Ajax Library (http://ajax.asp.net/downloads/library/default.aspx?tabid=47&subtabid=471) which is under the MSPL (which allows you to modify the files). Copy the System.Web.Extensions folder found in the Library zip into the folder you pointed ScriptPath to.

    Alternatively, if you don't want to have all your script references path-based, you can point only the core framework to a file and leave the others to use web resources as usual. This makes things easier when using other resource-based libraries such as the toolkit. This is easily done by adding the following script reference to your script manager:

    <asp:ScriptReference
        Name="MicrosoftAjax.js" ScriptMode="Auto"
       
    Path="~/[Your Script Directory]/System.Web.Extensions/1.0.61025.0/MicrosoftAjax.js"/>

    Of course, don't forget to replace the part of the path between the brackets with the name of the script directory you chose. Don't set a ScriptPath on the script manager if you choose to use this script reference. 

    Once you’ve done that, you can check that the application still works and loads the script from the new location using a network monitoring tool such as Fiddler.

    The second step is to patch the files. You’ll need to patch the debug and release versions.

    The debug version is MicrosoftAjax.debug.js. Look for the following code:

    switch(Sys.Browser.agent) {
        case Sys.Browser.InternetExplorer:
    

    then replace everything between that and "case Sys.Browser.Safari:" with the following code:

    Sys.UI.DomElement.getLocation = function(element) {
        if (element.self || element.nodeType === 9) return new Sys.UI.Point(0,0);
        var clientRect = element.getBoundingClientRect();
        if (!clientRect) {
            return new Sys.UI.Point(0,0);
        }
        var ownerDocument = element.document.documentElement;
        var offsetX = clientRect.left - 2 + ownerDocument.scrollLeft,
            offsetY = clientRect.top - 2 + ownerDocument.scrollTop;
        
        try {
            var f = element.ownerDocument.parentWindow.frameElement || null;
            if (f) {
                var offset = 2 - (f.frameBorder || 1) * 2;
                offsetX += offset;
                offsetY += offset;
            }
        }
        catch(ex) {
        }    
        
        return new Sys.UI.Point(offsetX, offsetY);
    }
    break;
    

    For the release version (MicrosoftAjax.js), the process is pretty much the same except that the file is a little more difficult to manipulate. Look for "switch(Sys.Browser.agent){case Sys.Browser.InternetExplorer:" and replace everything between that and "case Sys.Browser.Safari:" with the following:

    Sys.UI.DomElement.getLocation=function(a){if(a.self||a.nodeType===9)return new Sys.UI.Point(0,0);var b=a.getBoundingClientRect();if(!b)return new Sys.UI.Point(0,0);var c=a.document.documentElement,d=b.left-2+c.scrollLeft,e=b.top-2+c.scrollTop;try{var g=a.ownerDocument.parentWindow.frameElement||null;if(g){var f=2-(g.frameBorder||1)*2;d+=f;e+=f}}catch(h){}return new Sys.UI.Point(d,e)};break;

    (no line breaks)

    The site should now work without the exceptions.

    Known issues with that fix

    • Coordinates returned by Sys.UI.DomElement.getLocation may be off by two pixels in some scenarios involving frames from different domains.
    • The patched implementation returns the upper left coordinates of the bounding box around the element instead of the upper left corner of the first rectangle of the element, which can be different for wrappable elements. This is inconsistent with what the function returns on other browsers.
    • If you enable script localization on the script manager, it will generate a request for a localized version of the Ajax script, which will not exist because we haven't shipped localized versions of the standalone script files yet. You can work around this by renaming the files MicrosoftAjax.en.js and MicrosoftAjax.debug.en.js and adding ResourceUICultures="en" to the script reference. Don't change the path or name.

    Important disclaimer

    This fix implies that you stop using the resource-based scripts and use the static file versions instead.  I expect this is the fix that will be in the next service release, so when the next release of System.Web.Extensions happens, you will want to revert to using the resource-based scripts to get any other fixes or changes that are made.

    UPDATE: added a way to replace just the core framework file.

    UPDATE 2: The toolkit had a similar bug, and now provides a similar workaround: http://blogs.msdn.com/delay/archive/2007/02/05/safely-avoiding-the-access-denied-dialog-how-to-work-around-the-access-denied-cross-domain-iframe-issue-in-the-ajax-control-toolkit.aspx

    UPDATE 3: made the release patching procedure clearer based on Atanu's comment. Apologies to everyone who hit this and thanks to Atanu for pointing it out.

    UPDATE 4: added known issue with localization and how to work around it.

    UPDATE 5: this is fixed in ASP.NET 3.5.

    Read more...

  • We shipped!

    I'm very proud to announce that ASP.NET Ajax 1.0 shipped this morning. This is the result of all the work we've done since the first CTP and of all the very valuable feedback from our early adopters. I can't stress this enough: this product is probably one of the Microsoft products that incorporated the most user feedback. It is also to my knowledge the first time that Microsoft releases the full source code for a supported product (yes, we have full 24x7 support for ten years).

    Read more...