Detecting Global Pollution with the JScript RuntimeObject Sunday, 11 April 2010

This article is about debugging with JScript's RuntimeObject (msdn). All of the examples work in IE 5.5+, though most do not work in any other browser.

Leaked Global Identifiers

Say you accidentally created a global property, as in the following:

function playRugby(players) {
  var items,
      i;
      len = items.length; // Global.
}

function kick() {
  var x = 10
      y = 11; // ASI makes y global.
}
When playRugby is called, a global property len is created, if it does not already exist, and then assigned the value of items.length. Likewise, when kick is called, a global property y is created.

These globals are unintentional. They break encapsulation and leak implementation details. This can result in conflict and awkward dependency issues.

To detect accidentally created global identifiers, we can loop over the global object using for in. Firebug provides this convenient global inspection under the "DOM" tab.

Everybody's Favorite Browser

Unfortunately, in IE, the for in won't enumerate any global variables or function declarations, as seen in the example below.

Example Enumerating the Global Object

// Property of global variable object.
var EX1_GLOBAL_VARIABLE = 10;

// Property of global object.
this.EX1_GLOBAL_PROPERTY = 11;

// Property of global variable object.
function EX1_GLOBAL_FUNCTION(){}

(function(){
  var results = [];
  for(var p in this) {
    results.push(p);
  }
  alert("Leaked:\n" + results.join("\n"));
})();

The result in IE contains a mix of window properties and one the four user-defined properties: EX1_GLOBAL_PROPERTY.

So what happened to the other three user-defined properties? Why didn't they show up in the for in loop?

It turns out that enumerating over the global object will enumerate properties assigned to the global object and will not enumerate global variables.

An educated guess as to why global properties are enumerated but global variables are not might be that JScript gives global variables (declared with var), the DontEnum flag. Since the global object is specified as being the global Variable object, this seems like a likely explanation. It would be nonstandard, but it would explain the behavior in IE. Eric Lippert, however, provided a different explanation: The global object and the global variable object are two different objects in IE.

According to MS-ES3:

JScript 5.x variable instantiations creates properties of the global object that have the DontEnum attribute.

Enumeration Solution: The JScript RuntimeObject

To enumerate over global properties, use the JScript RuntimeObject method. Instead of enumerating over the global object, as you would use in a normal implementation, enumerate over an object returned by the global RuntimeObject method.

var GLOBAL_VAR1, 
    GLOBAL_VAR2, 
    GLOBAL_VAR3 = 1; 
    GLOBAL_PROP1 = 12;

function GLOBAL_FUNCTION(){}

if(this.RuntimeObject){
    void function() {
        var ro = RuntimeObject(),
            results = [],
            prop;
        for(prop in ro) {
            results.push(prop);
        }
        alert("leaked:\n" + results.join("\n"));
    }();
}
IE Result

The result in IE 8 and below includes (among other things, including window) GLOBAL_FUNCTION, GLOBAL_VAR3, and GLOBAL_PROP1, in that order, as they were evaluated in. Notice that neither GLOBAL_VAR1 nor GLOBAL_VAR2 were included. It appears that RuntimeObject does not accumulate any variables that were unassigned to. According to Microsoft's documentation, this is not the specified behavior (more on this below).

Microsoft RuntimeObject Documentation

The JScript RuntimeObject is a built-in extension to JScript. JScript defines seven additional built-in global methods: ScriptEngine, ScriptEngineBuildVersion, ScriptEngineMajorVersion, ScriptEngineMinorVersion, CollectGarbage, RuntimeObject, and GetObject. These objects are all native JScript objects, not to be confused with host objects.

For RuntimeObject, Microsoft JScript Extensions [MS-ES3EX] states:

The RuntimeObject function is used to search a global object for properties with names that match a specified pattern. The function only locates properties of the global object that were explicitly created by VariableStatement or FunctionDeclaration functions, or that were implicitly created by appearing as an identifier on the left side of an assignment operator. The function does not locate properties that were created by means of explicit property access on the global object.

Superficial testing indicates that Microsoft's documentation is wrong.

The returned object does not includes all identifiers that were added to the Variable object; only those identifiers that have been assigned a value. Whether or not they were created from VariableDeclaration, FunctionDeclaration, or assignment as global properties does not matter.

Example of Finding Identifiers Created By FunctionBindingList

All identifiers in a FunctionBindingList of a JScriptFunction will become properties of the containing Variable object, so, for example:

var foo = {}, undef, ro;
(function(){ function foo.bar, baz(){} })();
ro = RuntimeObject();
alert([ro.foo.bar, "undef" in ro].join("\n"));
IE elerts
function foo.bar(){}
false

Browsers other than IE running JScript can be expected to throw SyntaxError upon parsing the FunctionBindingList of JScriptFunction production. This is to be expected, as it is a syntax extension.

Bookmarklet

As a bookmarklet:
javascript:(function() {var ro=RuntimeObject(),r=[],i=0,p;for(p in ro){r[i++]=p;}alert('leaked:\n'+r.join('\n'));})();
JScript Syntax Extension

The earlier example "Finding Identifiers Created By FunctionBindingList" mentioned the JScript Extension JScriptFunction. In case the name is not a dead giveaway, this is a JScript language extension. The production for JScriptFunction is:

JScriptFunction : 
function FunctionBindingList ( FormalParameterListopt ) { FunctionBody }
RuntimeObject(filterString): The filterString Parameter

The RuntimeObject method accepts an optional filter string to match identifiers. Unfortunately, filterString is not converted to a regular expression but is used for substring matching with optional leftWild and rightWild, defaulting to *.

This means that, for example: filterString = "a*" would match identifiers a and a1 but not ba.

Conclusion

Documentation bugs and shortcomings aside, the RuntimeObject provides a useful alternative to the problem of enumerating global properties in JScript. An advantage with RuntimeObject is that it only includes user-defined properties, with the exception of the global window property.

The aforementioned bookmarklet provides a convenient way to check a page to see the globals that have been accidentally created (it also shows that this site is not a shining example of keeping the global object clean).

Other Applications for RuntimeObject

Cross Browser Identifier Leak Bookmarklet

Writing a cross-browser identifier leak detector is the next logical step to an IE-only identifier leak detector.

Automated Identifier Leak Detection

Checking for accidental global identifiers should be automated.

The YUI Test unit test framework provides hooks for TEST_CASE_BEGIN_EVENT and TEST_CASE_COMPLETE_EVENT . These events can be used to inspect the RuntimeObject and catch global identifier leaks that occur througout the runtime execution of program code.

In TEST_CASE_BEGIN_EVENT, inspect the RuntimeObject and save the result. In TEST_CASE_COMPLETE_EVENT, inspect the RuntimeObject again and compare the results with results saved during TEST_CASE_BEGIN_EVENT. Next, for each property that appeared in TEST_CASE_COMPLETE_EVENT but was not present in the result saved from TEST_CASE_BEGIN_EVENT , a global identifier has been leaked and a test case warning can be logged.

References

  • [MS-ES3EX]: Microsoft JScript Extensions to the ECMAScript Language Specification Third Edition.
Posted by default at 4:23 PM in Browsers

Myopia and the Opera 10 User Agent String Friday, 29 May 2009

Opera has conceived a silly tactic planned for Opera 10 user-agent string.

The problem is that there are scripts that expect the browser major version to single-digit and will fail if it is not.

Since "10" not a single digit, these scripts fail.

Opera has mitigated that problem by changing the user-agent to 9.80 and publishing the following warning:

Browser sniffing ? unless you?re writing a web stats application ? is always a bad idea. It?s a misguided attempt to send different content to different user agents. This is never scalable ? you can?t change every website you?ve ever made every time a new browser version comes out. It is also not future-proof, as highlighted by this article.

Ineffectual and meaningless little blurb there. Those badly written sites that used (poor) browser detection will not break from Opera. Opera spoofing their own user-agent string helps reaffirm the misconception that the authors' browser detection worked. Posting up a little warning that not everyone will read does not make an example.

The blurb states that Browser detection is used "to send different content to different user agents". Not always true. In fact, browser detection is more often used on the client to work around an perceived incompatibility. Since Opera is wrong on that count, it makes the blurb seem even less relevant, as an author who read it might still try to justify or rationalize his approach by saying "but that's not why I used browser detection."

Browser detection scripts cause forwards-compatibility and maintenance problems. However, to not be able to parse out a number is not only not smart, it shows very poor coding skill.

Opera states that version 11 will have "11" in the user-agent string.

My opinion is somewhat in line with Doug's on this one (that Yahoo 360 URL is an awful URL).

If you are a developer, check your code. It really isn't hard to do this stuff correctly. It really isn't.

Where I disagree with Doug is "Opera has been forced to lie."

Opera developers made a decision to lie, as explained by Opera. They were not forced.

An alternative to that choice is for Opera to not cater to badly authored pages and simply let them break.

Breaking sites is bad in the short term because it renders pages unusable. However, it is good in the larger scheme of the web in the long run. By driving home a hard lesson, Opera could teach developers to not use browser detection by providing an historical lesson.

The first sensible opinion on the matter was Hallvord's post from December, 2008., where he pointed out that Bank of America and Live.com failed in Opera 10. The entry describes the reason: Faulty parsing of the User-Agent string, and redirecting to the "not supported page".

You'd think that with the intense development Microsoft has been lavishing on live.com they would have found somebody capable of writing a usable browser sniffer (or ideally a person clever enough to say "wait, we don't really need one - what if we just use feature detection instead?"). Think again..

Of course, Microsoft has been advocating detection "best practices" for years, despite well reasoned arguments to stop doing that (G. Talbot, T. Zijdel).

Opera should be less myopic and stop worrying about breaking badly authored sites. Web developers should be less myopic, and build maintainable, forwards-compatible solutions.

Posted by default at 12:06 AM in Browsers

Browser Detection Saturday, 24 November 2007

There have been many, many, articles and discussions that eschew browser detection, yet it continues.

Despite all of this information, browser detection can be seen in most of the popular JavaScript libraries including Prototype, YUI, Dojo, jQuery, and is present in applications such as the newly refactored GMail:

For a better Gmail experience, use a fully supported browser.

Browser detection is bad for many reasons:

  1. Browsers change.
  2. The User Agent string does not represent the browser reliably.
  3. Even if it did, the browser doesn't represent feature support (See #1).

Browser detection is unrelated to the problem it is trying to solve.

Browser detection makes code hard to maintain. It accomplishes this by requiring that the next version of [insert_browser_name] will also have to be tested and special-cased in the code.

Alternative: Feature Detection

For example, does the browser support opacity? This can easily be determined:

if("opacity"in el.style) { }

Support of opacity has nothing to do with whether that browser actually is an IE version, nor is the reverse true: IE does not imply support (or lack of support) for opacity.

Detection for feature support does not suffer from maintenance problems when Internet Explorer decides to support opacity. Capability detection takes feature detection one step further.

Once the code has been properly designed and tested, it should not be a problem to maintain.

I have learned this the hard way and have tried to remove browser detection from my drag code, though evidence of my mistake is still present. I had to refactor my drag code in specific cases where it checked for browsers identifying with an Opera User Agent string (removed checks to ua.opera). My code still contains one conditional branch that needs to be refactored, As is, the script works in all of the modern browsers (this is subject to change).

With browser detection, the internal quality of the code suffers, even if the code works. This is because it introduces a dynamic aspect that must be maintained as the browsers change. Implementation Matters.

ToBoolean

Be careful when testing for values of properties. Some values may evaluate to false in a boolean context.

// Error-prone, scrollTop may be 0, which would evaluate to false
if (document.body.scrollTop) {
    // statements that work with scrollTop property
}

The new GMail

New code should definitely not rely on browser detection

GMail, which was recently redesigned, still uses browser detection and also punishes users with the performance hit of a misused HTTP redirect (HTTP/1.x 302 Moved Temporarily), or, if GMail finds your browser's User Agent header unsuitable, it two HTTP redirects.

In fact, when developing for mobile phones, I have found Chris Penderick's UserAgent switcher useful. Unfortunately, this confuses GMail, messing up the rendering and even encoding of messages.

GMail seems to be predominantly developed with a windows-centric mentality. This is evidenced by the lack of support for Command + S to save in Mac, and the Safari and Opera bugs witnessed in earlier versions of GMail.

Google Groups

Google Groups also relies on faulty browser detection to block certain features.

Google Groups Browser Error, in a Draggable, Floating DHTML Pane Google Groups Browser Error, in a Draggable, Floating DHTML Pane

This feature is not supported in your browser. Download a copy of Firefox or Internet Explorer to upload your picture.

I find it ironic that my browser can be assumed to support the draggable, floating pane, and not suitable for uploading a picture. The floating pane is draggable from anywhere inside it, so it is impossible for a user who gets the error to select the error message text.

Acknowledgements

Google deserves proper recognition for providing a clear example of why browser detection is a bad practice.

Contrasting Example

For proof that an effective Ajax application can be developed without browser detection, have a look at Google Reader.

Browser Detection is mostly a bad idea.

In the meantime, I'm looking for a decent mail application that runs in the browser. Both Yahoo mail and GMail fall short of my expectations.

Technorati Tags:

Posted by default at 9:40 PM in Browsers

Opera Bug: getComputedStyle Returns Margin for Unset Top/Left Values Thursday, 8 November 2007

In Opera 9.2 getComputedStyle(el, '').getPropertyValue returns the margin value for top/left values when the top/left values aren't set. In Safari the returned values are 'auto' in this case.

Example

testcase showing bug in Opera and Safari

Workaround

The way to avoid the problem is to explicitly add top and left values to the stylesheet:

#Test {
  top: 0;
  left: 0;
}

Then getComputedStyle will return correct values for top/left (0px) in Opera 9 and Safari.

pixelXXX

A convenient alternative would be a currentStyle.pixelLeft. Only Opera and IE support currentStyle and only Opera supports currentStyle.pixelLeft. (IE supports style.pixelLeft; this only reads from the style attribute)

Mozilla does not support pixelXXX properties at all, though Opera, IE, and Safari 3 all do.

Technorati Tags:

Posted by default at 6:25 PM in Browsers

Browser Stats Thursday, 18 October 2007

Browser Stats

Firefox is #1. Trumps MSIE.

 BrowsersGrabberHitsPercent
FFFirefoxNo7985734.6 %
MS Internet ExplorerNo7497732.5 %
Unknown?6332827.4 %
SafariNo40061.7 %
OperaNo36071.5 %
MozillaNo31081.3 %
NetNewsWireNo5170.2 %
KonquerorNo4220.1 %
CaminoNo2520.1 %
NetscapeNo910 %
 Others 1230 %

Opera's settings allow surfers to easily spoof the User-Agent header. Opera is probably slightly underrepresented.

Technorati Tags:

Posted by default at 2:05 AM in Browsers

 

*AnimTree
*Tabs
*GlideMenus
*DragLib