Wednesday, June 22, 2011

How Facebook's xd_proxy.php seemed to have broken IE8...

The problem related to IE8, 32-bit and 64-bit versions of Internet Explorer with Flash installed when invoking the FB.getLoginStatus() call. Facebook recently broke IE8 with a non-closing xd_proxy.php. I just did a diff compare between the last set of changes today from a 6/17 snapshot and noticed this line change:
<         document.XdComm.postMessage_init('FB.XD.Flash.onMessage', FB.XD._origin);
---
>         document.XdComm.postMessage_init('FB.XD.Flash.onMessage', FB.XD._openerOrigin ? FB.XD._openerOrigin : FB.XD._origin);
But why would this line do anything? The issues point to problems with IE8 (not IE7) and Flash, which may explain why I didn't see any issues on IE7. IE9, which I was also running on my desktop but did not have Flash installed, may also explain why I didn't encounter the problem.

Facebook relies on the HTML5 postMessage functionality to send cross-browser JavaScript code between different window browsers visiting different domains. For Firefox/Chrome, HTML5 support works great to pass JavaScript code from Facebook and inject it into your local app. But with IE8, the postMessage seems to have issues with popups so Facebook has gone for two different approaches, one with a Flash widget that mimics the same behavior as postMessage and using IFrame's. The latter works for IE with no Flash, the former requires a minimum of Adobe Flash 9.0.159.0 or 10.0.22.87.

Well, it turns out Facebook on the last push last week added this section of code, which tries to handle specific cases of using IE8:
var a = !! window.attachEvent;
    if (FB.XD._transport != 'postmessage' && a && window.postMessage) {
      FB.XD._openerTransport = FB.XD._transport;
      FB.XD._openerOrigin = FB.XD._origin;
      FB.XD._nonOpenerOrigin = d;
    }
(The method of transport is 'flash', it supports the window.attachEvent, and two !! means double-negated, and IE8 apparently does support window.postMessage):

...which is part of this section:
var d = (window.location.protocol + '//' + window.location.host + '/' + FB.guid());
  if (window.addEventListener && !window.attachEvent && window.postMessage) {
      FB.XD._origin = d;
      FB.XD.PostMessage.init();
      FB.XD._transport = 'postmessage';
    } else if (!b && FB.Flash.hasMinVersion()) {
      if (document.getElementById('fb-root')) {
        var c = document.domain;
        if (c == 'facebook.com') c = window.location.host;
        FB.XD._origin = (window.location.protocol + '//' + c + '/' + FB.guid());
        FB.XD.Flash.init();
        FB.XD._transport = 'flash';
      } else {
        if (FB.log) FB.log('missing fb-root, defaulting to fragment-based xdcomm');
        FB.XD._transport = 'fragment';
        FB.XD.Fragment._channelUrl = b || window.location.toString();
      }
    } else {
      FB.XD._transport = 'fragment';
      FB.XD.Fragment._channelUrl = b || window.location.toString();
    }
    var a = !! window.attachEvent;
    if (FB.XD._transport != 'postmessage' && a && window.postMessage) {
      FB.XD._openerTransport = FB.XD._transport;
      FB.XD._openerOrigin = FB.XD._origin;
      FB.XD._nonOpenerOrigin = d;
    }

(Background info: The line (c == 'facebook.com') checks to see if the window you're opening is www.facebook.com, which the JavaScript on their site seems to set document.domain to facebook.com (There is JS in their site that sets document.domain=window.location.hostname.replace(/^.*(facebook\..*)$/i,'$1')). If it is, then it sets the c variable to www.facebook.com (or any other facebook.com site you're looking at). Otherwise, it sets the origin location from document.domain, which is usually the page you're loading.

Also, within their cross-domain handler (xdHandler), Facebook also added these lines:
if (FB.XD._openerTransport) if (e == 'opener') {
FB.XD._transport = FB.XD._openerTransport;
FB.XD._origin = FB.XD._openerOrigin;
} else {
FB.XD.PostMessage.init();
cFB.XD._transport = 'postmessage';
FB.XD._origin = FB.XD._nonOpenerOrigin;
}
So XD._origin can be either FB.XD_openerOrigin or FB.XD_nonOpenerOrigin....in this latest change that broke IE8, it caused some issue. In the latest revision, they gave higher precedence to FB.XD_openerOrigin, since FB.XD._origin can switch between the two different values.

The trouble is created if getLoginStatus() is invoked before the Facebook Connect button is clicked, since the Flash receiver is expecting the value of FB.XD._origin when it was first initialized. The FB.guid() calls basically generate a random 16-character string, which explains why there are two different string values for FB.XD._openerOrigin and FB.XD._nonOpenerOrigin.

FB.XD._origin = (window.location.protocol + '//' + c + '/' + FB.guid());
        FB.XD.Flash.init();
But since getLoginStatus creates three different callback routines, XD._origin can be overwritten:
e = FB.Auth.xdHandler;
      FB.copy(a.params, {
        no_session: e(b, c, 'parent', false, 'notConnected'),
        no_user: e(b, c, 'parent', false, 'unknown'),
        ok_session: e(b, c, 'parent', false, 'connected'),
You can confirm this issue by putting breakpoints before/after the xdHandler no_session, no_user, and ok_session callbacks are initialized:
>>FB.XD._openerOrigin
"http://dev.myhost.com/f27227ed548279c"
>>FB.XD._origin
"http://dev.myhost.com/f1b7fa085f189da"

...so when the Facebook Connect button is loaded with the Flash cross-domain receiver, FB.XD._origin is not its original value:
FB.Flash.onReady(function() {
        document.XdComm.postMessage_init('FB.XD.Flash.onMessage', FB.XD._origin);
      });
Presumably the message passed to the Flash receiver is used to verify that the 'opener' succeeded, triggering the Flash callbacks (FB.XD._callbacks) to continue. Since the FB.XD._origin is different than the one that was used at initialization, the Flash XD receiver seems not to fire (I don't have the .swf file disassembled, so can't confirm).

Subsequently, the entire sign-on process fails. The problem did not happen until last week when the connect.js library began to change FB.XD._origin within the xdHandler() code, which was not caught apparently because it was not tested with getLoginStatus() before deploying.

(For background info only: The FB.XD._origin also gets passed into the URL query string when you click the Facebook connect, and usually it gets checked by Facebook to see if it's a valid origin URL in your Facebook app. You could login to your Facebook Connect site (if you host on myhost.com and try through myotherhost.com) and observe the same phenemonon with the XD proxy. Now Facebook warns you with a "Given URL is not allowed by the Application configuration." Regardless, the FB.XD._origin seems to be used by the Flash callback routine to determine whether to fire).

So the lesson learned is that in your browser testing, you have to cover all bases with checking against IE7/IE8/IE9 with and without Flash (minimum of 9.0.159.0 or 10.0.22.87 -- yes, the code seems supports a minimum of either of both versions). You can disable/enable the Adobe/Shockwave Flash inside the Add-Ons section of Internet Explorer. If you have customer support issues, you can also point them to this link http://www.adobe.com/software/flash/about/(or shortened to http://adobe.ly/9uzSR3) to help figure out what Flash version customers are using. Windows 7 comes with both the 32-bit and 64-bit on Internet Explorer, so you can run both to test (though keep in mind there is only a 64-bit preview Flash release on Adobe's web site).

One thing I've also done is added a cronjob task to download versions of Facebook Connect so that you can deminify and compare diffs. This approach will hopefully let developers stay on top of Facebook Connect changes. You should also download a JavaScript compiler and deminify the code so that the diffs are easy to compare!
# m h  dom mon dow   command
0 2 * * 0 bash -c "/usr/bin/wget https://connect.facebook.net/en_US/all.js -O ~/js/all_`date --rfc-3339=date`.js"

Also, in case you want the last 4 snapshots of the all.js library, I've posted them here:

http://bit.ly/mhH6re
The files are as follows:
all.js.4.deminify is the one that fixed the issue.
all.js.3.deminify created the IE8 bug.
all.js.2.deminify was the previous version before it all happened.

You can look at the timestamps of each file at the top:
import datetime
datetime.fromtimestamp(1308323215)
>>> datetime.datetime.fromtimestamp(1308323215)
datetime.datetime(2011, 6, 17, 8, 6, 55)
You can also setup your own ability to track Facebook Connect changes too (thanks to Nate Friedly):
http://hustoknow.blogspot.com/2011/06/daily-snapshots-of-facebook-connect.html

Want to know more? More details too about Facebook's SWF Flash cross-domain plugin....
http://hustoknow.blogspot.com/2011/06/deconstructing-facebooks-flash-cross.html

2 comments:

  1. Thanks, you rock!

    ReplyDelete
  2. Thanks for the explanation, Roger.

    Is this working for you?

    Looks like it's back.

    ReplyDelete