UserJS.org

Overriding functions and variables

Written on 2005-07-13 09:43 by tarquin. Last modified 2005-09-19 09:19

One of the most powerful features of Opera's User JavaScript implementation is the ability to patch existing scripts on web pages. It can intercept the page's scripts before they are loaded, and make modifications to them using string manipulation.

Most of the time, scripts that fail in Opera would work if only the author allowed Opera to run them, or with simple modifications to make the scripts more compatible. It is often possible to fix these by making Opera pretend it is another browser (which can then cause problems for other pages or other User JavaScripts), or by altering the page's scripts to make them identify Opera as another browser.

Repeatedly running replace commands on the page's scripts can become very costly in terms of performance, especially since the scripts can be several kilobytes long. To make it easier to patch scripts without needing to use string replacements, Opera provides User JavaScripts with two extra methods; opera.defineMagicFunction and opera.defineMagicVariable.

Basic variables

Overriding normal global variables is very easy, using defineMagicVariable. Say, for example, that the page is checking to see if the browser is capable of performing DHTML, and has the ability to reflow the page. To do this, it checks for document.all and document.getElementyId but excludes Opera, since Opera 6 could not do reflow, and the page author has not updated the script for several years to allow it to work in Opera 7 and 8.

var canDoDHTML = (document.all || document.getElementById) && !window.opera;

This mistaken script could be patched in several ways. The first would be to delete the window.opera object, but this would cause other User JavaScripts to produce errors, and may break other scripts on the page. The second would be to use string replacements to replace the "window.opera" check with "false", but this may require a replacement in a very long script, and may be very slow to run. The third and best approach would be to use defineMagicVariable to redefine the canDoDHTML variable.

opera.defineMagicVariable(
  'canDoDHTML',
  function () { return true; },
  null
);

This would not stop the script making the mistaken check in the first place, but every time the script tried to check if the browser had passed the test, the User JavaScript would return true, so the rest of the script would be allowed to run in Opera.

Using the setter

Sometimes you may need to do something whenever a script attempts to set the value of a variable. For example, if a script attempts to set the location directly, you may want to intercept it, and only allow it to be set under certain circumstances. You can use the setter function do do this. Opera will pass the new value of the variable as the first parameter to the magic variable's setter function. In the same way, it will pass the current value of the variable as the first parameter to the getter function:

opera.defineMagicVariable(
  'location',
  function (curVal) { return curVal; },
  function (newVal) {
    if( newVal != 'upgrade.html' ) { location.href = newVal; }
  }
);

Note that you must not set the variable itself inside the setter function, or get the variable value inside the getter function, as this would result in an infinite loop.

Inside the setter and getter functions, the 'this' keyword refers to a local object. It is unique to each magic variable, and can be used to store information that you can then pass between the methods if you want to. For example, you could use it to store the number of times the script had tried to store a new value in the variable. You could then return that value whenever the script tried to access the content of the variable:

opera.defineMagicVariable(
  'myvariable',
  function () { return this.hitCount; },
  function () {
    if( !this.hitCount ) { this.hitCount = 0; }
    this.hitCount++;
  }
);

Variables that are sometimes correct

Sometimes a script makes a mistake because a variable holds a value that it did not expect. That variable may be correct under some circumstances, so in order to fix it, it would be necessary to check what value it holds and only correct it when necessary.

For example, a script may be checking for the existence of an attribute, and it may use the following code:

var theAttribute = oNode.getAttribute('href');
if( theAttribute != null ) {

This is actually invalid, since getAttribute should never return null, it should return an empty string if the attribute has not been specified, and as a reault, the test would always pass, even if the attribute was not specified (since an empty string does not evaluate to null). Unfortunately, Internet Eplorer and Mozilla/Firefox incorrectly return null, so some scripts will expect this incorrect response, and will behave incorrectly in Opera. This could easily be fixed by replacing the test with if( theAttribute ) { but this would mean running a regular expression replacement against the entire string.

In this case, it is more easy to simply replace the variable's value with null if it is an empty string. We can still return the correct value if it is not empty:

opera.defineMagicVariable(
  'theAttribute',
  function (curVal) { 
    return curVal ? curVal : null;
  },
  null
);

Basic functions

Overriding normal global functions is also very easy, using defineMagicFunction. You can define a new function that will be used instead of the normal function.

For example, if the page is going to use a function called setBookMark that attempts to create a bookmark using a function provided by Internet Explorer, you can redefine the function so it works in Opera as well. This example assumes the setBookMark function accepts two parameters, the first being the bookmark title, and the second being the bookmark address:

opera.defineMagicFunction(
  'setBookMark',
  function (oRealFunction,oThis,oTitle,oBookmark) {
    var foo = document.createElement('a');
    foo.setAttribute('rel','sidebar');
    foo.setAttribute('href',oBookmark);
    foo.setAttribute('title',oTitle);
    foo.click();
  }
);

You can also tell your new function to run the original function:

oRealFunction.apply( oThis, arguments.slice(2) );

Object properties

The defineMagicVariable method only works for global variables, not for their properties. So you cannot attempt to redefine someObject.someProperty using defineMagicVariable. Or at least not directly. However, you can overwrite the parent object, and define a getter for that. Since the getter will be called every time a script attempts to access the child property, you can overwrite the property in the getter:

opera.defineMagicVariable(
  'someObject',
  function (curVal) {
    if( curVal ) {
      <span class="js-comment">//we cannot assign properties to null or undefined objects</span>
      if( curVal.someProperty == 'IE' ) {
        <span class="js-comment">//we can check the existing property value</span>
        curVal.someProperty = 'DOM';
        <span class="js-comment">//and we can overwrite it</span>
      }
    }
    <span class="js-comment">//return the (modified) object</span>
    return curVal;
  },
  null
);

Setters for object properties

Opera does not support __defineSetter__ or the watch method, and defineMagicVariable does not work with specific properties, so this is not strictly possible. The most reliable way is to replace the source code of the script element with a modified version that does not have the problem. Unfortunately, this is not as efficient as defineMagicVariable would have been:

opera.addEventListener(
  'BeforeScript',
  function (e) {
    e.element.text.replace(/location.href='upgrade.html'/,'location.href='index2.html'');
  },
  false
);

In some rare cases, it may be possible to define a setter for a property, assuming that the parent object does not need to be referenced directly. For example, if you want to detect when a script is about to set the location.href property, you may be able to use a workaround.

Firstly, store a reference to the parent object, so that you can access it without causing errors (an anonymous function can be used to ensure the variable is not made global). Then use defineMagicVariable to define a getter for the parent that returns the window object instead (the setter is not important). Now whenever the script tries to access parentObject.childProperty, it ends up accessing window.childProperty instead.

Now use defineMagicVariable to define a global variable with the same name as the child that needs to be accessed. This can then use a setter and getter to do whatever it needs, and it can use the stored reference to get or set the actual values whenever they are needed:

(function () {

var realLocation = location;

opera.defineMagicVariable(
  'location',
  function (curVal) { return window; },
  null
);

opera.defineMagicVariable(
  'href',
  function (curVal) { return realLocation.href; },
  function (newVal) { if( newVal != 'upgrade.html' ) { realLocation.href = newVal; } }
);

})();

Note: this assumes that the script (and other User JavaScripts) will never need to access the parent object only (as this User JavaScript will replace it with a reference to the window object). It also assumes that there is no existing global variable with the same name as the child property. Lastly, it also assumes that you will use defineMagicVariable for all the children of the parent object, so that the script can use any of them it needs.

Object methods

This is essentially the same as object properties:

opera.defineMagicVariable(
  'someObject',
  function (curVal) {
    if( curVal ) {
      curVal.someMethod = function () {
      	return false;
      };
    }
    return curVal;
  },
  null
);

20.07.2005

Additionally, you can also run the original method if needed, in the same way as you can with defineMagicFunction. Note however, that you will override the existing method, so you need to obtain a reference to it first that you can keep. You must not overwrite it again later (unless it has been replaced with a new value), or you risk creating an infinite loop.

opera.defineMagicVariable(
  'someObject',
  function (curVal) {
    if( curVal && curVal.someMethod && !curVal.oldReplacedMethod ) {
      curVal.oldReplacedMethod = curVal.someMethod;
      curVal.someMethod = function (param1,param2) {
      	return curVal.oldReplacedMethod(param1,param2);
      };
    }
    return curVal;
  },
  null
);

Local variables

If a variable is not global, and only exists within a local scope (such as a variable inside a function), you cannot use defineMagicVariable to override it, or create setters or getters for it.

function foo(test1,test2) {
  var bar = 'baz';
  ...
}

Unfortunately, I do not know of any way to emulate the defineMagicVariable behaviour within a specific scope. If I find a way, I will update this tutorial to describe how. Until then, there are two possibilities that I can think of. One is to redefine the entire function without the problem. The other is to replace the source code of the script element with a modified version that does not have the problem. Unfortunately, neither of these are ideal. For the first one, it may be a very large function that you need to redefine, and for the second, it will be less efficient:

opera.addEventListener(
  'BeforeScript',
  function (e) {
    e.element.text.replace(/var bar = 'baz';/,'var bar = 'qux';');
  },
  false
);

Of course, this may be inefficient, since it runs a regular expression replace against every script that is loaded. It would be better if the replacement could be made against just the one function, and even better, only if the function actually gets used.

This can be done using defineMagicFunction. The first time the function is run, create a fixed function, and then use that each time instead. The source code of the function can be obtained using the toString method of the function, and a replacement can be made using the Function constructor.

opera.defineMagicFunction(
  'foo',
  function (oRealFunction,oThis) {
    if( !this.newfoo ) {
      <span class="js-comment">//get the function as a (formatted) string ('function foo(test1,test2) { ... }')</span>
      var functionString = oRealFunction.toString();
      <span class="js-comment">//get just the contents</span>
      functionString = functionString.replace(/(^s*function[^{]*{|}s*$)/g,'');
      <span class="js-comment">//fix the problem - remember that Opera reformats the function code</span>
      functionString = functionString.replace(/var bar = "baz";/,'var bar = 'qux';');
      <span class="js-comment">//make a new function with the repaired code</span>
      this.newfoo = new Function('test1','test2',functionString);
    }
    <span class="js-comment">//call your new fixed function</span>
    return this.newfoo.apply( oThis, arguments.slice(2) );
  }
);

Other object properties

Occasionally, you may need to overwrite the property of an object that you cannot easily reference. For example, when a script is attempting to reference a nonstandard attribute of an element, and it uses the direct property name instead of the getAttribute method (this is allowed in Internet Explorer, but not standards compliant browsers like Opera).

if( document.getElementById('foo').bar )

There are a few possible ways you can deal with this problem. Firstly, you can replace the source code of the script element with a modified version that does not have the problem. Secondly, you can hopefully step through the DOM yourself, and fix it before the script tries to use it - this may not always be possible. Thirdly, you can use defineMagicVariable to override the parent object, and define the property manually when the variable is accessed. This could be fairly wasteful depending on how often it is accessed:

opera.defineMagicVariable(
  'document',
  function (curVal) {
    var baz = curVal.getElementById('foo');
    baz.bar = baz.getAttribute('bar');
    return curVal;
  },
  false
}

Note, to make it more efficient, it may also be possible to attach this behaviour to another, unrelated variable that is accessed only once, before the part of the script that uses this reference.