Patching start.com

This article describes a User JavaScript used to patch www.start.com. It demonstrates the power of Opera's User JavaScript, intercepting the scripts that the page is loading and rewriting them to fix problems before they occur.
Author: TarquinWJ

Start.com is a nice idea, and when the site works, it is a very impressive interface. Of course, it is totally inaccessible, as not only does it fail completely if JavaScript is not available, it also relies on the user being able to use a mouse to click on the page, since it does not use real links or buttons for most of the interface. However, that is not the main problem.

The problem is that the site was programmed with only two browser families in mind; Internet Explorer and Mozilla. Mozilla, and derivatives such as Firefox, try to follow the W3C DOM, JavaScript and ECMAscript standards as closely as possible. Internet Explorer is significantly further behind, but implements a few extensions of its own to replace parts of the missing functionality. Opera supports both the standards compliant scripts, and many of the Internet Explorer extensions.

The script uses several of these features and there are several correct ways that it could deal with the incompatibilities. The best and easiest way would be to check each property as it tries to use them, and use the alternative if the first is not available. But instead, the script attempts to patch Mozilla to make it emulate Internet Explorer's behaviour.

This means that it uses Mozilla's __defineSetter__ and __defineGetter__ methods to create properties that Internet Explorer provides. That includes the srcElement, offsetX/Y, and returnValue properties of the EventObject, the pixelHeight/Width/Left/Top properties of the style object, and innerText. Unlike Mozilla, Opera 7 and 8 already provide these properties, but not the __defineSetter__ and __defineGetter__ methods.

When the page is loaded in Opera, it shows the "loading" state, and this does not change. The JavaScript console points to the use of the __defineSetter__ and __defineGetter__ methods. So even though Opera does not need these to be used, the page attempts to use them anyway, because it thinks Opera is Mozilla. The JavaScript file responsible is very badly formatted, but after some cleanup, this line is found to be the culprit:

MSN.Browser.IsMozilla = function() {
  return
	( typeof document.implementation != 'undefined' ) &&
	( typeof document.implementation.createDocument != 'undefined' ) &&
	( typeof HTMLDocument !=' undefined');
};

This assumption is totally incorrect, since Mozilla is not the only browser that supports these properties and objects. They are, after all, part of the W3C DOM standard. However, the page now uses this mistake to decide whether or not to emulate Internet Explorer's behaviour, even though at no point does it use the document.implementation property that it tested for. If written properly, this page could have worked in Opera 8 and above, Safari 1.2 and above, Konqueror 3.3 and above, as well as Internet Explorer and Mozilla, but instead, these other browsers are faced with invalid code, that produces errors.

To prevent this incorrect detection, a User JavaScript can intercept the loading of the script, and replace the check with one that always fails:

window.opera.addEventListener(
  'BeforeScript',
  function (e) {
	e.element.text = e.element.text.replace(/document\.implementation/g,'document.foobargone');
...

Now with that one fix out of the way, the page gets a little closer to loading. However, it then fails because Opera does actually need a few functions that were provided for Mozilla. Not many, so these can be easily copied into the User JavaScript file.

window.selectSingleNode = function (d,v,c){v+="[1]";var nl=selectNodes(d,v,c);if(nl.length>0)return nl[0];else return null;}
Document.prototype.selectSingleNode = function(v){return selectSingleNode(this,v,null);}
Element.prototype.selectSingleNode = function(v){var scope=this.ownerDocument;if(scope.selectSingleNode)return selectSingleNode(scope,v,this);else return null;}

These all eventually refer to a single function; selectNodes. This function uses XPath to access a Node collection. Opera 8 does not yet support XPath, but the page is using almost none of the functionality of XPath, so it becomes relatively easy to emulate it for Opera. The page looks for this pattern:

group[@name = 'Start'][1]

This should reference the first element with the tagName "group", and the "name" attribute set to "Start". Regular expressions can isolate the required parts, getElementsByTagName can reference the elements, then a for loop can search through all the elements and use getAttribute to check if it is the desired tag. The following function is a direct replacement for the one used on the page:

window.selectNodes = function (d,v,c){
  var elName = v.replace(/[^\w].*$/,'');
  elName = d.getElementsByTagName(elName);
  var attrToMatch = v.replace(/^.*@/,'').replace(/\s*=.*$/,'');
  var valToMatch = v.replace(/^[^']*'/,'').replace(/'.*$/,'');
  for(var i=0;elName[i];i++){if(elName[i].getAttribute(attrToMatch)==valToMatch){return [elName[i]];}}
  return [];
}

Now the main part of the page functions, except that everything is blank. The page uses responseXML.xml to check if the XML loaded correctly. It had to emulate this properietary property for Mozilla. For Opera (or any of the browsers), a simple replacement can be made to make the page check if the responseXML has any childNodes:

e.element.text = e.element.text.replace(/response\.responseXML\.xml == \"\"/g,'response.responseXML.childNodes.length == 0');

Now to actually obtain the data from the XML, the page uses either the "text" or "textContent" properties, as well as the "innerText" property. Opera 8 requires the "innerText" property, so this needs to be used wherever "text" was used (the script could easily use the W3C DOM childNodes collections to do the same thing, and that would work in all the browsers, without needing to allow for incompatibilities):

e.element.text = e.element.text.replace(/\.text,/g,'.innerText,');
e.element.text = e.element.text.replace(/\.text;/g,'.innerText;');

Unfortunately, because of a bug in Opera 8, entities are preserved in innerText, even though they should not be. As a result, all the feeds loaded by the page appear as HTML source code, not as rendered text. To fix this, the String class is extended to add an extra method; removeEntities. This method creates a div element in memory, fills it with the contents of the string. It then reads out the childNodes of the div element, to obtain the clean text. The method also checks if this bug exists before trying to fix it, so when the bug in Opera gets fixed, the function will not need to be changed:

e.element.text = e.element.text.replace(/\.innerText;/g,'.innerText.removeEntities();');
...
String.prototype.removeEntities = function () {
  var oDiv = document.createElement('div'), oStr = this.toString();
  oDiv.innerHTML = '>';
  if( oDiv.innerText != '>' ) {
	//broken innerText, fix it
	oDiv.innerHTML = oStr;
	oStr = '';
	for( var i = 0; oDiv.childNodes[i]; i++ ) {
	  oStr += oDiv.childNodes[i].nodeValue;
	}
  }
  delete oDiv;
  return oStr;
};

As a final problem, Opera 8.0 does not implement the setRequestHeader method for XMLHttpRequest (it actually does as of V8.02!). The script uses this, although it works without it. A string replacement on the source code can make the script check for the existance of this method before attempting to use it:

e.element.text = e.element.text.replace(/xml\.setRequestHeader/g,'if(xml.setRequestHeader)xml.setRequestHeader');

So finally, the User JavaScript file is finished, and is a complete patch for start.com, allowing it to work in Opera 8 without causing any errors. Download it, add it to your User JavaScript folder, and enjoy. Purely for the purpose of asthetics, the User JavaScript also corrects a wrong assumption about body element default margins and paddings. You can find that fix for yourself.

Categories

CategoryTutorial
CategoryOpera

Backlinks
There are 8 comments on this page. [Display and/or add comments]