Paul Robertson's words, punctuated

Thoughts on development, user-centered design, code, etc. by Paul Robertson

Problem (and solution) : getURL() in a Flash projector fails in Firefox

On Windows, using getURL() or a similar approach to open an HTML page from a SWF running in the standalone Flash Player (such as a Flash projector file running on a CD-ROM) doesn’t work if the user has Firefox set as their default browser. Here we’ll take a look at what’s causing the problem and how to work around it.

Note: based on some helpful feedback the code was revised on Oct. 30, 2006 to be more cross-platform-friendly.

As a followup to a question about opening local and remote resources in ActionScript, I was asked why a SWF which had been made into a projector (a standalone executable file) wasn’t able to open an HTML page using getURL() when the user’s default browser is Firefox. One initial idea was that it might be related to the security restrictions added in Flash Player 8, which (by default) make it so a SWF which is running off a user’s local computer can only access local resources or network resources, but not both. However, as the documentation points out, a SWF which has been published as an executable file isn’t subject to the same restrictions. (And I actually tested it out to confirm that’s the case.)

So here’s the situation: imagine you’re creating a SWF file to use as a menu on a CD-ROM. It will have several buttons, some of which will open HTML pages that are also on the CD-ROM. That sounds easy enough–using the getURL() function in ActionScript 2 (or the equivalent flash.net.navigateToURL() function in ActionScript 3.0), you can specify a relative url (basically just the file name) of the HTML page you want to open, and when the user clicks the button, Flash Player hands the url off to the operating system and the HTML page is opened in the user’s default browser. So if you want to open a file named “test.html” which is at the same level in the file system as the SWF executable, you would use this code (ActionScript 2):

// assume the button's instance name is myButton

myButton.onRelease = function()
{
    getURL("test.html", "_blank");
};

or this code (ActionScript 3.0):

// assume the button's name is myButton

import flash.events.MouseEvent;
import flash.net.*;

function openPage(event:MouseEvent):void
{
    var request:URLRequest = new URLRequest("test.html");
    navigateToURL(request, "_blank");
}

myButton.addEventListener(MouseEvent.CLICK, openPage);

Assuming the user’s default browser is Internet Explorer (or a version of Netscape/Mozilla prior to the ones based on Firefox), this works as you would expect it to: the user clicks the button, and the page “test.html” opens in the browser.

However, if the user’s default browser is Firefox (or, I suspect, one of the recent Netscape versions based on Firefox) then the page does not open. Instead, one of a few possible things happens. On my system, Firefox opens with two tabs. The first tab is blank, and the second contains a security warning (something about not being able to change the location of the My Documents folder). This is very possibly because I was testing it from my hard drive, rather than from a CD-ROM drive. On other people’s systems, from what I’ve heard, the browser opens with a blank tab and a tab which is the result of a Google “I’m feeling lucky” search based on the name of the file (which is the default action when you enter keywords rather than a valid url in the browser’s address bar).

Note: this problem isn’t just limited to getURL() – it also applies to other methods which load a url, such as LoadVars.send().

So what’s going on, and more importantly, how do we make it work so that we can use SWF menus to open web pages on a CD-ROM? After doing a little research, I learned first of all that Adobe is aware of the situation, and that in fact it’s not a Flash Player bug at all. It turns out that this is due to a Firefox bug (well, an out-of-control feature) based on how Firefox handles a url which is passed to it as a command-line parameter.

According to the bug report, when you call Firefox on the command line, you can pass it multiple urls separated by pipe (|) characters and it will open each one in a separate tab. So if you issue a command like this:

firefox -url "http://www.yahoo.com/|http://www.google.com/"

Firefox opens a new browser window, with Yahoo in one tab and Google in another.

Going back to Flash Player, when you call the getURL()/navigateToURL() function with a relative url (something like “test.html”), Flash Player actually constructs an absolute url which is what it passes to the browser. So when our code says getURL("test.html", "_blank"), Flash Player turns that into something like this in the (Windows) operating system:

firefox -url "file:///d|assets\test.html"

Notice the magical pipe character (|) where you or I would probably put a colon (i.e. “file:///d:\assets\test.html”). The pipe syntax is legal–Firefox just assumes that a colon will be used instead. So when Flash Player constructs an absolute url and sends it to Firefox, Firefox sees the pipe character and says “hey, they want me to open two tabs, the first with the url “file:///d” and the second with the url “assets\test.html”. Hence we see a browser window with two tabs, with the second containing the result of a Google search for “assets\test.html”.

Note: according to the same bug report, this has been fixed for the “Deer Park” version of Firefox, which I believe is Firefox 2.0, so once that browser is out and spread throughout the world, this won’t be as much of an issue. But I won’t be holding my breath…

Interestingly, on the ActionScript end of things the problem stems from the fact that Flash Player is constructing an absolute url to send to the browser, and that url contains a pipe character. When you specify an absolute url in your getURL()/navigateToURL() call, Flash Player just passes that url to the browser unmodified. Of course that’s what we’d expect for a remote web address, like “http://www.google.com/”, but it works just the same for local “file:///” urls. So to work around the Firefox bug, what we really need to do is construct the absolute url ourselves in ActionScript, and use that url (which, again, will get passed to the browser unmodified). The big question, of course, is how we can figure out the absolute path of a file on a CD-ROM when we don’t necessarily know the drive letter of the user’s CD-ROM drive.

Fortunately, ActionScript comes to our rescue. Every MovieClip object (every DisplayObject in AS3) knows the url it was loaded from. You can access this as the _url property in ActionScript 2, or as the url property of the DisplayObject’s loaderInfo property in ActionScript 3.0. Usually, for a SWF in a web page, this will be the url of the SWF file, of course, but when a SWF is running locally (even when it’s in an executable/projector) that property contains an absolute “file:///” url with the path of the SWF file on the file system. For example, consider this code, on a keyframe on the main timeline, running in a SWF projector (an EXE file) on a CD-ROM:

// Assumes there's a dynamic text field on the stage named output_txt

// AS2
output_txt.text = this._url; // output_txt contains "file:///E|/assets/trial.exe"

// AS3
output_txt.text = this.loaderInfo.url; // output_txt contains "file:///E|/assets/trial.exe"

Going back to our SWF CD-ROM menu example, if the HTML page we want to open is in the same folder as the SWF projector, we can just generate our own absolute url by getting the SWF’s url, chopping off the name of the SWF, replacing the “|” with a colon, and adding the name of the HTML page. (Or, for a more complex case, we can use the SWF’s url to determine the “base” url, and then use that to generate absolute urls for files in other folders etc.)

Here is the code for a working version of this (ActionScript 2):

// code on a keyframe on the main timeline

var swfUrl:String = _root._url;
var lastSlashIndex:Number = swfUrl.lastIndexOf("/");
var pipeIndex:Number = swfUrl.indexOf("|");
var baseUrl:String;
if (pipeIndex >= 0)
{
    baseUrl = swfUrl.substring(0, pipeIndex);
    baseUrl += ":";
}
else
{
    baseUrl = "";
}
baseUrl += swfUrl.substring(pipeIndex + 1, lastSlashIndex + 1);

myButton.onRelease = function()
{
    var targetUrl:String = baseUrl + "test.html";
    getURL(targetUrl, "_blank");
};

And here’s a working version in ActionScript 3.0:

// code on a keyframe on the main timeline

import flash.events.MouseEvent;
import flash.net.*;

output_txt.text = this.loaderInfo.url;

var swfUrl:String = this.root.loaderInfo.url;
var lastSlashIndex:Number = swfUrl.lastIndexOf("/");
var pipeIndex:Number = swfUrl.indexOf("|");
var baseUrl:String;
if (pipeIndex >= 0)
{
    baseUrl = swfUrl.substring(0, pipeIndex);
    baseUrl += ":";
}
else
{
    baseUrl = "";
}
baseUrl += swfUrl.substring(pipeIndex + 1, lastSlashIndex + 1);

function gotoTestHtml(event:MouseEvent):void
{
    var targetUrl:URLRequest = new URLRequest(baseUrl + "test.html");
    navigateToURL(targetUrl, "_blank");
}

myButton.addEventListener(MouseEvent.CLICK, gotoTestHtml);

Of course for the AS3 version you could use regular expressions for the finding and replacing–but in this case it’s simple enough, and perhaps more efficient, to not use them.

If you’d like to try it out for yourself, you can download FLAs for these two versions (45 KB .zip).

By request, I’ve also made a version of the FLA in Flash MX 2004 format (31 KB .zip).

Comments