miscoded

the web is a hack

Subscribe to RSS feed

Posts tagged with "browsers"

The new Google Maps patch, dissected

, ,

We released a new browser.js file yesterday. It's a pretty interesting release actually..

The Amazon patch is a pretty radical step - we have never launched any patch that embeds a third-party JS library on every page of a global web presence like Amazon. We know it solves some of our problems, and we know it doesn't solve all problems - we'll keep working with Amazon to have more of their server-side browser sniffing fixed. It would be nice if this patch were short-lived wink


But the most interesting patch - or at least the hardest one to come up with - is a workaround for a bug in the Carakan ECMAScript engine that a new Google Maps feature hit. The feature is being rolled out, so some users have seen this problem and some haven't yet. As you see from the screenshot, they are very excited about the new feature and announce it with a small popup. In Opera, that popup refuses to go away again. Using the close button just brings up the small "loading" indication at the top of the screenshot. Using Google Maps with this stubborn box staying on top of everything else is more than a little annoying..

My colleague Kåre did the hard work of reading obfuscated JS to figure out where the problem was. I don't want to know how many hours it took him to boil it down to this compact demo:
var passed = 0;
(function (){
	var foo = 1;
	this.runtest = function (){
		eval('window.ev=function(a){eval(a)};');
		try{
			window.ev('(function(){passed=foo})()')
			}
		catch(e){}
		document.getElementsByTagName('p')[0].firstChild.data = passed?'PASS':'FAIL';
		};
	})();
runtest();


To understand this issue, including why finding a patch at first seemed impossible, we need to know about an esoteric quirk of the eval() function.

Normally, if you're naughty enough to call eval() (which it is better not to use, by the way..) you invoke the window.eval() built-in function. It will evaluate the code you pass to it in the local scope. So, for example doing this:
function(){
  var ok=true;
  eval('alert(ok?"sees local variable":"does not see local variable")');
}

the eval'ed code sees the local variable 'ok'.

However, eval by any other name doesn't smell as sweet - if the name you use to refer to the eval function is not 'eval' the method will change behaviour entirely, and run code in the global scope instead! Doing this:
var ok=false;
function(){
  var ok=true, ev=window.eval;
  ev('alert(ok?"sees local variable":"does not see local variable")');
}

we see that if the reference to the eval method is different, it no longer sees local variables!

This odd behaviour is a known eval quirk which is now standardised in ECMAScript 5.

Regarding the problem, Google Maps seem to have found a way to do indirect eval in a local scope. Sort of. I have no idea why they are doing it this way..

The window.ev('(function(){passed=foo})()') just references another method that will do the actual eval'ing. Doing an indirect eval here would run code in global scope. Hence it should not actually have accesss to the local 'foo' variable.

However, the earlier eval call is a direct eval - so it will run in the local scope:
eval('window.ev=function(a){eval(a)};');
meaning that the function expression here should have closure data that would include the local variable foo.

Hence, when a string of code that refers to 'foo' is passed to the direct eval() inside that ev function, this eval call will run in the local scope of that function expression, and the variable foo should be found in its closure data. And this is where Carakan stumbles: it fails somehow to get the closure data right for a function expression created in an eval() inside another function.

Now, why was this hard to patch? Normally when some method Opera defines has a bug, we can overwrite that method with a fixed one from browser.js. For example, if we find a problem in the DOM appendChild() method we'd just overwrite Node.prototype.appendChild with a working method. However, we can not overwrite window.eval() like that - because we would need to cache a reference to it, and any calls to that reference would be an "indirect" eval and run in the global scope.. This would violate most of Google Maps assumptions and make the site fail entirely.

Here's the patch I eventually came up with:
addPreprocessHandler(  /function\(a\)\{eval\(a\)\}/g, '(function(){return function(a){eval(a)}})());' );


This does a search/replace on Google Maps' source code while we load it, replacing any instances of
function(a){eval(a)}

with
(function(){return function(a){eval(a)}})());

which adds an extra function expression - one more chance for Carakan to record closure data. This double-take does, by a lucky stroke, save the required context.

So, here's hoping most users get the new browser.js before the Google Maps update. I like the speeeed of patching problems before users even get them smile

alignment of megaliths

, , , ...

The main point of the standards is of course to align the megaliths rendering and scripting engines of standards-compliant browsers. I've already discussed some of the problems arising when specs and implementations clash, and the dilemmas we face when we have the choice between being strictly standards-compliant and breaking some major website or breaking the spec, aligning with the other browsers, and making the site(s) work.

Never an easy decision to make!

I can now confirm a few points where Opera 9.5 is going to deviate somewhat from the specs or from our earlier implementations in order to be better aligned with other browsers. If you are a webmaster or JavaScript author please check that the changes will not cause problems for you!

We'll do the following:

  • Capturing event listeners will fire on target. This behaviour will be kept until Firefox fixes their bug. Opera's new implementation is not expected to cause problems since most sites using the addEventListener API will have been tested against Firefox. A possible exception is widget code which may expect Opera's old behaviour.
  • Implementation of capturing load event handlers changed: load events from within the document will now only be captured when the capturing event listener is attached to the document. In other words, earlier you could do
    window.addEventListener('load', func, true)
    and expect func to be called for all "load" events inside the document. From 9.5 you will have to do
    document.addEventListener('load', func, true)
    to get this capturing behaviour. (Please do not copy that code unless you know what the "true" part of it means!) This will probably become the official specification. Again, this will probably only break code specifically written for Opera such as widgets, and the change is known to fix large number of websites that use event capture by mistake.
  • We've changed the relationship between body.clientHeight and documentElement.clientHeight in standards mode. The value of body.clientHeight is now viewport height, documentElement.clientHeight is document height. This is AFAIK what Firefox does too. This change can break your website if you use browser detection before reading clientHeight from body or documentElement.


I believe Firefox will support adding capturing load events to document. There you go, alignment of megaliths!

(Like the post title? Found it on Wikipedia smile.)

When specs and implementations clash

, , , ...

One of the points of having open, independently developed standards from the W3C is of course to achieve compatibility.

However, standards are not always clear and consistent, and browsers do not always get things right.

When facing a choice between being compatible with other browsers and websites OR the standard, we may have a third way: fixing the standard to align it with the common implementations and the content. That's the only way one can have the cake and eat it, be standards-compatible and compatible with the Web as it is. Is it feasible? Or even a good idea? Standards are supposed to be set-in-stone after all, to keep implementors happy so that we don't spend hours and money doing it this way only to find a rewritten standard tells us to follow that way instead..

Three real-life examples of cases where we do break or are about to break the standard:

1) getAttribute on non-existing attributes
Standard mandated returning an empty string. Other browsers returned null. Sites running into problems due to Opera's correct standards support included Yahoo mail.

We reached a general consensus that returning null is a better idea, so we have changed Opera and asked the relevant W3C group to consider changing the text in an errata or the next version.

2) Throwing WRONG_DOCUMENT_ERR on cross-document node usage
If a node created in one document is appended to another document, the standard clearly states that a WRONG_DOCUMENT_ERR should be thrown. Firefox doesn't, and sites taking the Firefox behaviour for granted and breaking in Opera included Blogger.com's rich text editor.

Opera will start to call adoptNode implicitly. I'm not sure what way the standard will go - nobody seems to intend to push for a change here. According to their bug report, Mozilla intends to fix it (which is great!) and as soon as they do and websites fix their coding errors, we can revert to spec-compatible mode. Meanwhile, we are not going to live with sites being broken in Opera due to Firefox's bugs.

3) addEventListener and firing capturing events on target
Setting a capturing "click" event listener on, say, an IMG tag has no effect, because the standard clearly says that event listeners should not fire on target. Again, Firefox gets the implementation wrong and sites where this has caused trouble include live.com (according to the Safari team, I have not seen this myself but their analysis is probably right).

Sure, we should fix the standard here and go for it - I don't see any specific benefit to not firing the event listener though I'm all ears if anyone knows why the spec is written like that. We haven't changed this yet but I guess we will follow Safari.

The lesson? To standards bodies: please care about existing implementations, experience and content when developing the standards. Specs that are seriously incompatible with prior implementations or web content cause implementation headaches, incompatibilities and suffering users.

FireFox non-standard event object property breaks KLM navigation in Opera

, , , ...

This makes me really annoyed: after fixing bugs and patching and in general trying hard to make things work, KLM's website is broken AGAIN. The navigation at the top does not unfold in Opera, making most of the site inaccessible.

The reason turns out to be an error happening here:

Navigation.prototype.open = function (e) {
if (e) if (e.originalTarget.innerHTML == "Home") return;
if (e) if (e.originalTarget.id == "navigation") return;

(from this JS file )

What, I ask myself, is this originalTarget property that we don't support? It's nowhere in the W3C DOM events specification. Even a Google search doesn't throw up much information. It's obviously a Mozilla-thing though.

So, we're again playing catch-up with a new, unknown and badly documented extension to the standard. We have to reverse-engineer it with test cases to understand what is going on. Meanwhile, important sites will be broken because they insist on relying on non-standard code whatever we say. Users might abandon Opera because of it, or not switch to us in the first place.

You know Mozilla, this sounds an awful lot like the game IE has been playing with us for all those years. If anything you're worse than Microsoft because MSDN usually has at least a minimum of official documentation. And you still claim to be "an advocate for standards on the Net"?? Yeah, right.

Dell goes for the quirks

, , , ...

This page should have a Flash in it. Now Opera, where is it?

Well, here's a pretty little function from Dell, meant to return an element with a specific name:

function ParseDivObjects( targetName )
{
var objs = document.getElementsByName( targetName );

//-- Workaround for "inconsistent" W3C ruleset in IE as DIV's aren't mapped with name properties
if( objs.length == 0 )
{
var targets = document.getElementsByTagName( "DIV" );
var tmpObj;

objs = new Array();

for( var targetidx = 0; targetidx < targets.length; targetidx++ )
{
tmpObj = targets[targetidx];
if( tmpObj.name == targetName )
{
objs.push( tmpObj );
}
}
}
return objs;
}


Let's start: document.getElementsByName is not supposed to be used for elements that are not supposed to have a "name" attribute. So, for example you can use getElementsByName with INPUT elements but not with DIV. Except that Mozilla apparently disagrees with that interpretation of the specification and thinks IE's behaviour is "a quirk". Really?

Anyway, Dell has a workaround for IE: they go through all DIVs in the page and check tmpObj.name, which means Opera is out of luck again. Since DIVs are not meant to have a "name" attribute we don't create any .name property for it. Using getAttribute here would make it work.

So Dell crams two different quirks into one function, and Opera fails because it supports neither. Time to violate the DOM spec a little more.. sad

sniff and die

, , , ...

Well, I didn't quite get to the bottom of the Live.com problems last time but now I know: it's a bug in their browser sniffer. They say

Web.Browser._isIE=!Web.Browser.isMozilla()&&!Web.Browser._isOpera;


but mean

Web.Browser._isIE=!Web.Browser.isMozilla()&&!Web.Browser.isOpera();


.. so isIE is set to true even though we're Opera. That means the script gets into the wrong branches of the getElementsByTagName stuff in my previous post.

Yet another proof that all the world's evils come from browser sniffing. If sniffing wasn't necessary, we would have peace in the Middle East, no starvation in Africa .. OK, sorry, I'm getting carried away here but at the very least Live.com would be alive and kicking.

dead live

, , , ...

live.com was fixed to work in Opera but apparently recent changes have broken it again. Here is why:

  • They declare a namespace without associating a namespace URI with it: <html xmlns:Web> in a normal text/html document
  • When they use the namespace in <web:binding>, Opera will not return that element when you do document.getElementsByTagName('binding')
  • (Also note that they use a capital letter in the namespace declaration and lower case in the document)

Now, this would work fine if they sent an XHTML content-type, added a namespace URI and fixed the case issue.

But is it a bug in Opera? I haven't been able to interpret the xmlns spec in enough detail to understand whether this is covered. Or is it in the DOM spec? Validation certainly seems to say it is wrong to declare a namespace without URI.

The only thing I know for sure is that FireFox does the same as Opera, and that might indicate that a standard is being followed. Perhaps readers more skilled at xmlns juggling can enlighten me, if not I'll learn when developers comment on the bug report..

ticket to break

,

Just wanted to share my most bizarre online shopping experience ever.. Looking for Oslo-Osaka flights, my wife found some relatively reasonable prices on a website called ticket2travel and left the booking to me. I was in IE, not because my better half doesn't use Opera but because the search form for flights uses a JavaScript for submit, and the script uses the reserved word goto to identify a form element named "goto", and thus one can not submit the form in browsers who care about such errors. 1-0 to IE? Just you wait..

So, I filled in relevant details and completed the booking procedure - nearly. When the tickets were confirmed with the airline and I expected a payment form, Internet Explorer decided to go minimalist and presented me with an entirely empty, white popup window. What gives?

View source. Of course. That's the sort of reflex my type of work gives you. When in doubt about a website's intentions, view source. Parse the scripts in your brain. Sniff out the errors..

But there was nothing whatsoever wrong with the source. They had sent IE a complete page, and IE just refused to render it! (If the initial step doesn't work in Opera and the final not in IE what browser DID they test with? FireFox only?)

So what now? I'm just a payment away from those tickets I want. IE is broken. Is it safe to reload this page, or will I buy another set of tickets if I do? Toggling the few styles/colours settings in IE doesn't help either..

Well, what about using Opera..? But now I have a history and a session in IE, I need those cookies..

So, I steal my own cookies from IE with a bookmarklet, use Opera's cookie manager to add these cookies to Opera, copy the address over and we're off. Opera shows that very page beautifully and I can complete the booking procedure. (Turns out they want you to choose a travel agency to handle payment though, it was a disappointment since I expected to pay there and be done with it. Afterwards I've had to chase the order by phone, so all in all an example of how online shopping should NOT be done..)

The morale: caring about cross-browser compatibility isn't just about allowing customers to use their preferred browser. It is about making your site more robust against unexpected errors, because if one browser fails for whatever reason you have given the customer other possibilities.