/* # -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is Mozilla.org Code. # # The Initial Developer of the Original Code is # Doron Rosenberg. # Portions created by the Initial Developer are Copyright (C) 2001 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Bill Law # Scott MacGregor # Ben Goodger (2.0) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** */ /* This file implements the nsIHelperAppLauncherDialog interface. * * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog, * comprised of: * - a JS constructor function * - a prototype providing all the interface methods and implementation stuff * * In addition, this file implements an nsIModule object that registers the * nsUnknownContentTypeDialog component. */ /* ctor */ function nsUnknownContentTypeDialog() { // Initialize data properties. this.mLauncher = null; this.mContext = null; this.mSourcePath = null; this.chosenApp = null; this.givenDefaultApp = false; this.updateSelf = true; this.mTitle = ""; } nsUnknownContentTypeDialog.prototype = { nsIMIMEInfo : Components.interfaces.nsIMIMEInfo, // This "class" supports nsIHelperAppLauncherDialog, and nsISupports. QueryInterface: function (iid) { if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) && !iid.equals(Components.interfaces.nsISupports)) { throw Components.results.NS_ERROR_NO_INTERFACE; } return this; }, // ---------- nsIHelperAppLauncherDialog methods ---------- // show: Open XUL dialog using window watcher. Since the dialog is not // modal, it needs to be a top level window and the way to open // one of those is via that route). show: function(aLauncher, aContext) { this.mLauncher = aLauncher; this.mContext = aContext; // Display the dialog using the Window Watcher interface. var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher); this.mDialog = ww.openWindow(null, // no parent "chrome://mozapps/content/downloads/unknownContentType.xul", null, "chrome,titlebar,dialog=yes", null); // Hook this object to the dialog. this.mDialog.dialog = this; // Hook up utility functions. // XXXben these lines can disappear if we can get XULPP to run on stuff not // thus referenced in jar.mns. this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey; this.mIsMac = (this.mDialog.navigator.platform.indexOf("Mac") != -1); this.mIsWin = (this.mDialog.navigator.platform.indexOf("Win") != -1); // Watch for error notifications. this.progressListener.helperAppDlg = this; this.mLauncher.setWebProgressListener(this.progressListener); }, // promptForSaveToFile: Display file picker dialog and return selected file. // This is called by the External Helper App Service // after the ucth dialog calls |saveToDisk| with a null // target filename (no target, therefore user must pick). // // Alternatively, if the user has selected to have all // files download to a specific location, return that // location and don't ask via the dialog. // // Note - this function is called without a dialog, so it cannot access any part // of the dialog XUL as other functions on this object do. promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension) { var result = ""; this.mLauncher = aLauncher; // If the user is always downloading to the same location, the default download // folder is stored in preferences. If a value is found stored, use that // automatically and don't ask via a dialog. const kDownloadFolderPref = "browser.download.defaultFolder"; var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); try { result = prefs.getComplexValue(kDownloadFolderPref, Components.interfaces.nsILocalFile); result.append(aDefaultFile); // Since we're automatically downloading, we don't get the file picker's // logic to check for existing files, so we need to do that here. // // Note - this code is identical to that in // browser/base/content/contentAreaUtils.js. // If you are updating this code, update that code too! We can't share code // here since this is called in a js component. while (result.exists()) { var parts = /.+-(\d+)(\..*)?$/.exec(result.leafName); if (parts) { result.leafName = result.leafName.replace(/((\d+)\.)/, function (str, p1, part, s) { return (parseInt(part) + 1) + "."; }); } else { result.leafName = result.leafName.replace(/\./, "-1$&"); } } } catch (e) { } if (!result) { // Use file picker to show dialog. var nsIFilePicker = Components.interfaces.nsIFilePicker; var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); var bundle = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService); bundle = bundle.createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties"); var windowTitle = bundle.GetStringFromName("saveDialogTitle"); var parent = aContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowInternal); picker.init(parent, windowTitle, nsIFilePicker.modeSave); picker.defaultString = aDefaultFile; if (aSuggestedFileExtension) { // aSuggestedFileExtension includes the period, so strip it picker.defaultExtension = aSuggestedFileExtension.substring(1); } else { try { picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension; } catch (ex) { } } var wildCardExtension = "*"; if (aSuggestedFileExtension) { wildCardExtension += aSuggestedFileExtension; picker.appendFilter(this.mLauncher.MIMEInfo.Description, wildCardExtension); } picker.appendFilters( nsIFilePicker.filterAll ); // Pull in the user's preferences and get the default download directory. var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); try { var startDir = prefs.getComplexValue("browser.download.dir", Components.interfaces.nsILocalFile); if (startDir.exists()) { picker.displayDirectory = startDir; } } catch(exception) { } var dlgResult = picker.show(); if (dlgResult == nsIFilePicker.returnCancel) { // null result means user cancelled. return null; } // Be sure to save the directory the user chose as the new browser.download.dir result = picker.file; if (result) { var newDir = result.parent; prefs.setComplexValue("browser.download.dir", Components.interfaces.nsILocalFile, newDir); } } return result; }, // ---------- implementation methods ---------- // Web progress listener so we can detect errors while mLauncher is // streaming the data to a temporary file. progressListener: { // Implementation properties. helperAppDlg: null, // nsIWebProgressListener methods. // Look for error notifications and display alert to user. onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) { if ( aStatus != Components.results.NS_OK ) { // Get prompt service. var prompter = Components.classes[ "@mozilla.org/embedcomp/prompt-service;1" ] .getService( Components.interfaces.nsIPromptService ); // Display error alert (using text supplied by back-end). prompter.alert( this.dialog, this.helperAppDlg.mTitle, aMessage ); // Close the dialog. this.helperAppDlg.onCancel(); if ( this.helperAppDlg.mDialog ) { this.helperAppDlg.mDialog.close(); } } }, // Ignore onProgressChange, onStateChange, onLocationChange, and onSecurityChange notifications. onProgressChange: function( aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress ) { }, onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) { }, onLocationChange: function( aWebProgress, aRequest, aLocation ) { }, onSecurityChange: function( aWebProgress, aRequest, state ) { } }, // initDialog: Fill various dialog fields with initial content. initDialog : function() { // Put file name in window title. var win = this.dialogElement( "unknownContentType" ); var suggestedFileName = this.mLauncher.suggestedFileName; // Some URIs do not implement nsIURL, so we can't just QI. var url = this.mLauncher.source; var fname = ""; this.mSourcePath = url.prePath; try { url = url.QueryInterface( Components.interfaces.nsIURL ); // A url, use file name from it. fname = url.fileName; this.mSourcePath += url.directory; } catch (ex) { // A generic uri, use path. fname = url.path; this.mSourcePath += url.path; } if (suggestedFileName) fname = suggestedFileName; this.mTitle = this.dialogElement("strings").getFormattedString("title", [fname]); win.setAttribute( "title", this.mTitle ); // Put content type, filename and location into intro. this.initIntro(url, fname); var iconString = "moz-icon://" + fname + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType; this.dialogElement("contentTypeImage").setAttribute("src", iconString); this.initAppAndSaveToDiskValues(); // Initialize "always ask me" box. This should always be disabled // and set to true for the ambiguous type application/octet-stream. // We don't also check for application/x-msdownload here since we // want users to be able to autodownload .exe files. var rememberChoice = this.dialogElement("rememberChoice"); if (this.mLauncher.MIMEInfo.MIMEType == "application/octet-stream") { rememberChoice.checked = false; rememberChoice.disabled = true; } else { rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling; } this.toggleRememberChoice(rememberChoice); // XXXben - menulist won't init properly, hack. var openHandler = this.dialogElement("openHandler"); openHandler.parentNode.removeChild(openHandler); var openParent = this.dialogElement("open").parentNode; openParent.appendChild(openHandler); this.mDialog.setTimeout("dialog.postShowCallback()", 0); }, postShowCallback: function () { // Position the window if it has never been positioned before (i.e. // before XUL persistence has picked up anything to fill the screenX/ // screenY attributes with. Assume if one is unset, the other will be too). if (this.mDialog.document.documentElement.getAttribute("screenX") == "") { if (this.mDialog.opener) { this.mDialog.moveToAlertPosition(); } else { this.mDialog.centerWindowOnScreen(); } } this.mDialog.sizeToContent(); // Set initial focus this.dialogElement("mode").focus(); }, // initIntro: initIntro: function(url, filename) { this.dialogElement( "location" ).value = filename; // if mSourcePath is a local file, then let's use the pretty path name instead of an ugly // url... var pathString = this.mSourcePath; try { var fileURL = url.QueryInterface(Components.interfaces.nsIFileURL); if (fileURL) { var fileObject = fileURL.file; if (fileObject) { var parentObject = fileObject.parent; if (parentObject) { pathString = parentObject.path; } } } } catch(ex) {} // Set the location text, which is separate from the intro text so it can be cropped var location = this.dialogElement( "source" ); location.value = pathString; // Show the type of file. var type = this.dialogElement("type"); var mimeInfo = this.mLauncher.MIMEInfo; // 1. Try to use the pretty description of the type, if one is available. var typeString = mimeInfo.Description; if (typeString == "") { // 2. If there is none, use the extension to identify the file, e.g. "ZIP file" var primaryExtension = ""; try { primaryExtension = mimeInfo.primaryExtension; } catch (ex) { } if (primaryExtension != "") typeString = primaryExtension.toUpperCase() + " file"; // 3. If we can't even do that, just give up and show the MIME type. else typeString = mimeInfo.MIMEType; } type.value = typeString; }, // Returns true if opening the default application makes sense. openWithDefaultOK: function() { var result; // The checking is different on Windows... if ( this.mIsWin ) { // Windows presents some special cases. // We need to prevent use of "system default" when the file is // executable (so the user doesn't launch nasty programs downloaded // from the web), and, enable use of "system default" if it isn't // executable (because we will prompt the user for the default app // in that case). // Need to get temporary file and check for executable-ness. var ignore1 = new Object; var ignore2 = new Object; var tmpFile = this.mLauncher.getDownloadInfo( ignore1, ignore2 ); // Default is Ok if the file isn't executable (and vice-versa). result = !tmpFile.isExecutable(); } else { // On other platforms, default is Ok if there is a default app. // Note that nsIMIMEInfo providers need to ensure that this holds true // on each platform. result = this.mLauncher.MIMEInfo.defaultApplicationHandler; } return result; }, // Set "default" application description field. initDefaultApp: function() { // Use description, if we can get one. var desc = this.mLauncher.MIMEInfo.defaultDescription; if (desc) { var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]); this.dialogElement("defaultHandler").label = defaultApp; } }, // getPath: getPath: function (aFile) { if (this.mIsMac) return aFile.leafName || aFile.path; return aFile.path; }, // initAppAndSaveToDiskValues: initAppAndSaveToDiskValues: function() { var modeGroup = this.dialogElement("mode"); // We don't let users open .exe files or random binary data directly // from the browser at the moment because of security concerns. var mimeType = this.mLauncher.MIMEInfo.MIMEType; if (mimeType == "application/octet-stream" || mimeType == "application/x-msdownload") { this.dialogElement("open").disabled = true; var openHandler = this.dialogElement("openHandler"); openHandler.disabled = true; openHandler.label = ""; modeGroup.selectedItem = this.dialogElement("save"); return; } // Fill in helper app info, if there is any. this.chosenApp = this.mLauncher.MIMEInfo.preferredApplicationHandler; // Initialize "default application" field. this.initDefaultApp(); var otherHandler = this.dialogElement("otherHandler"); // Fill application name textbox. if (this.chosenApp && this.chosenApp.path) { otherHandler.setAttribute("path", this.getPath(this.chosenApp)); otherHandler.label = this.chosenApp.leafName; otherHandler.hidden = false; } var useDefault = this.dialogElement("useSystemDefault"); var openHandler = this.dialogElement("openHandler"); openHandler.selectedIndex = 0; if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) { // Open (using system default). modeGroup.selectedItem = this.dialogElement("open"); } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) { // Open with given helper app. modeGroup.selectedItem = this.dialogElement("open"); openHandler.selectedIndex = 1; } else { // Save to disk. modeGroup.selectedItem = this.dialogElement("save"); } // If we don't have a "default app" then disable that choice. if (!this.openWithDefaultOK()) { var useDefault = this.dialogElement("defaultHandler"); var isSelected = useDefault.selected; // Disable that choice. useDefault.hidden = true; // If that's the default, then switch to "save to disk." if (isSelected) { openHandler.selectedIndex = 1; modeGroup.selectedItem = this.dialogElement("save"); } } // otherHandler is always disabled on Mac if (this.mIsMac) otherHandler.hidden = true; otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = this.isMac; this.updateOKButton(); }, // Returns the user-selected application helperAppChoice: function() { return this.chosenApp; }, get saveToDisk() { return this.dialogElement("save").selected; }, get useOtherHandler() { return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1; }, get useSystemDefault() { return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0; }, toggleRememberChoice: function (aCheckbox) { this.dialogElement("settingsChange").hidden = !aCheckbox.checked; this.mDialog.sizeToContent(); }, openHandlerCommand: function () { if (this.dialogElement("openHandler").selectedItem.id == "choose") this.chooseApp(); }, updateOKButton: function() { var ok = false; if (this.dialogElement("save").selected) { // This is always OK. ok = true; } else if (this.dialogElement("open").selected) { switch (this.dialogElement("openHandler").selectedIndex) { case 0: // No app need be specified in this case. ok = true; break; case 1: // only enable the OK button if we have a default app to use or if // the user chose an app.... ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path")); break; } } // Enable Ok button if ok to press. this.mDialog.document.documentElement.getButton("accept").disabled = !ok; }, // Returns true iff the user-specified helper app has been modified. appChanged: function() { return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler; }, updateMIMEInfo: function() { var needUpdate = false; // If current selection differs from what's in the mime info object, // then we need to update. if (this.saveToDisk) { needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk; if (needUpdate) this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk; } else if (this.useSystemDefault) { needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault; if (needUpdate) this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault; } else { // For "open with", we need to check both preferred action and whether the user chose // a new app. needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged(); if (needUpdate) { this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp; // App may have changed - Update application and description var app = this.helperAppChoice(); this.mLauncher.MIMEInfo.preferredApplicationHandler = app; this.mLauncher.MIMEInfo.applicationDescription = ""; } } // We will also need to update if the "always ask" flag has changed. needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked); // One last special case: If the input "always ask" flag was false, then we always // update. In that case we are displaying the helper app dialog for the first // time for this mime type and we need to store the user's action in the mimeTypes.rdf // data source (whether that action has changed or not; if it didn't change, then we need // to store the "always ask" flag so the helper app dialog will or won't display // next time, per the user's selection). needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling; // Make sure mime info has updated setting for the "always ask" flag. this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked; return needUpdate; }, // See if the user changed things, and if so, update the // mimeTypes.rdf entry for this mime type. updateHelperAppPref: function() { var ha = new this.mDialog.HelperApps(); ha.updateTypeInfo(this.mLauncher.MIMEInfo); }, // onOK: onOK: function() { // Verify typed app path, if necessary. if (this.useOtherHandler) { var helperApp = this.helperAppChoice(); if (!helperApp || !helperApp.exists()) { // Show alert and try again. var bundle = this.dialogElement("strings"); var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").path]); var svc = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService); svc.alert(this.mDialog, bundle.getString("badApp.title"), msg); // Disable the OK button. this.mDialog.document.documentElement.getButton("accept").disabled = true; this.dialogElement("mode").focus(); // Clear chosen application. this.chosenApp = null; // Leave dialog up. return false; } } // Remove our web progress listener (a progress dialog will be // taking over). this.mLauncher.setWebProgressListener(null); // saveToDisk and launchWithApplication can return errors in // certain circumstances (e.g. The user clicks cancel in the // "Save to Disk" dialog. In those cases, we don't want to // update the helper application preferences in the RDF file. try { var needUpdate = this.updateMIMEInfo(); if (this.dialogElement("save").selected) { // If we're using a default download location, create a path // for the file to be saved to to pass to |saveToDisk| - otherwise // we must ask the user to pick a save name. var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); var targetFile = null; try { targetFile = prefs.getComplexValue("browser.download.defaultFolder", Components.interfaces.nsILocalFile); targetFile.append(this.dialogElement("location").value); } catch(e) {} this.mLauncher.saveToDisk(targetFile, false); } else this.mLauncher.launchWithApplication(null, false); // Update user pref for this mime type (if necessary). We do not // store anything in the mime type preferences for the ambiguous // type application/octet-stream. We do NOT do this for // application/x-msdownload since we want users to be able to // autodownload these to disk. if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream") this.updateHelperAppPref(); } catch(e) { } // Unhook dialog from this object. this.mDialog.dialog = null; // Close up dialog by returning true. return true; }, // onCancel: onCancel: function() { // Remove our web progress listener. this.mLauncher.setWebProgressListener(null); // Cancel app launcher. try { this.mLauncher.Cancel(); } catch(exception) { } // Unhook dialog from this object. this.mDialog.dialog = null; // Close up dialog by returning true. return true; }, // dialogElement: Convenience. dialogElement: function(id) { return this.mDialog.document.getElementById(id); }, // chooseApp: Open file picker and prompt user for application. chooseApp: function() { var nsIFilePicker = Components.interfaces.nsIFilePicker; var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); fp.init(this.mDialog, this.dialogElement("strings").getString("chooseAppFilePickerTitle"), nsIFilePicker.modeOpen); fp.appendFilters(nsIFilePicker.filterApps); if (fp.show() == nsIFilePicker.returnOK && fp.file) { // Remember the file they chose to run. this.chosenApp = fp.file; // Update dialog. var otherHandler = this.dialogElement("otherHandler"); otherHandler.removeAttribute("hidden"); otherHandler.setAttribute("path", this.getPath(this.chosenApp)); otherHandler.label = this.chosenApp.leafName; this.dialogElement("openHandler").selectedIndex = 1; this.dialogElement("mode").selectedItem = this.dialogElement("open"); } }, // Turn this on to get debugging messages. debug: false, // Dump text (if debug is on). dump: function( text ) { if ( this.debug ) { dump( text ); } }, // dumpInfo: doDebug: function() { const nsIProgressDialog = Components.interfaces.nsIProgressDialog; // Open new progress dialog. var progress = Components.classes[ "@mozilla.org/progressdialog;1" ] .createInstance( nsIProgressDialog ); // Show it. progress.open( this.mDialog ); }, // dumpObj: dumpObj: function( spec ) { var val = ""; try { val = eval( "this."+spec ).toString(); } catch( exception ) { } this.dump( spec + "=" + val + "\n" ); }, // dumpObjectProperties dumpObjectProperties: function( desc, obj ) { for( prop in obj ) { this.dump( desc + "." + prop + "=" ); var val = ""; try { val = obj[ prop ]; } catch ( exception ) { } this.dump( val + "\n" ); } } } // This Component's module implementation. All the code below is used to get this // component registered and accessible via XPCOM. var module = { firstTime: true, // registerSelf: Register this component. registerSelf: function (compMgr, fileSpec, location, type) { if (this.firstTime) { this.firstTime = false; throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN; } compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar); compMgr.registerFactoryLocation( this.cid, "Unknown Content Type Dialog", this.contractId, fileSpec, location, type ); }, // getClassObject: Return this component's factory object. getClassObject: function (compMgr, cid, iid) { if (!cid.equals(this.cid)) { throw Components.results.NS_ERROR_NO_INTERFACE; } if (!iid.equals(Components.interfaces.nsIFactory)) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; } return this.factory; }, /* CID for this class */ cid: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"), /* Contract ID for this class */ contractId: "@mozilla.org/helperapplauncherdialog;1", /* factory object */ factory: { // createInstance: Return a new nsProgressDialog object. createInstance: function (outer, iid) { if (outer != null) throw Components.results.NS_ERROR_NO_AGGREGATION; return (new nsUnknownContentTypeDialog()).QueryInterface(iid); } }, // canUnload: n/a (returns true) canUnload: function(compMgr) { return true; } }; // NSGetModule: Return the nsIModule object. function NSGetModule(compMgr, fileSpec) { return module; }