michael@0: # -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", michael@0: "resource://gre/modules/BrowserUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Downloads", michael@0: "resource://gre/modules/Downloads.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadLastDir", michael@0: "resource://gre/modules/DownloadLastDir.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: var ContentAreaUtils = { michael@0: michael@0: // this is for backwards compatibility. michael@0: get ioService() { michael@0: return Services.io; michael@0: }, michael@0: michael@0: get stringBundle() { michael@0: delete this.stringBundle; michael@0: return this.stringBundle = michael@0: Services.strings.createBundle("chrome://global/locale/contentAreaCommands.properties"); michael@0: } michael@0: } michael@0: michael@0: function urlSecurityCheck(aURL, aPrincipal, aFlags) michael@0: { michael@0: return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags); michael@0: } michael@0: michael@0: /** michael@0: * Determine whether or not a given focused DOMWindow is in the content area. michael@0: **/ michael@0: function isContentFrame(aFocusedWindow) michael@0: { michael@0: if (!aFocusedWindow) michael@0: return false; michael@0: michael@0: return (aFocusedWindow.top == window.content); michael@0: } michael@0: michael@0: // Clientele: (Make sure you don't break any of these) michael@0: // - File -> Save Page/Frame As... michael@0: // - Context -> Save Page/Frame As... michael@0: // - Context -> Save Link As... michael@0: // - Alt-Click links in web pages michael@0: // - Alt-Click links in the UI michael@0: // michael@0: // Try saving each of these types: michael@0: // - A complete webpage using File->Save Page As, and Context->Save Page As michael@0: // - A webpage as HTML only using the above methods michael@0: // - A webpage as Text only using the above methods michael@0: // - An image with an extension (e.g. .jpg) in its file name, using michael@0: // Context->Save Image As... michael@0: // - An image without an extension (e.g. a banner ad on cnn.com) using michael@0: // the above method. michael@0: // - A linked document using Save Link As... michael@0: // - A linked document using Alt-click Save Link As... michael@0: // michael@0: function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, michael@0: aSkipPrompt, aReferrer, aSourceDocument) michael@0: { michael@0: internalSave(aURL, null, aFileName, null, null, aShouldBypassCache, michael@0: aFilePickerTitleKey, null, aReferrer, aSourceDocument, michael@0: aSkipPrompt, null); michael@0: } michael@0: michael@0: // Just like saveURL, but will get some info off the image before michael@0: // calling internalSave michael@0: // Clientele: (Make sure you don't break any of these) michael@0: // - Context -> Save Image As... michael@0: const imgICache = Components.interfaces.imgICache; michael@0: const nsISupportsCString = Components.interfaces.nsISupportsCString; michael@0: michael@0: function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, michael@0: aSkipPrompt, aReferrer, aDoc) michael@0: { michael@0: var contentType = null; michael@0: var contentDisposition = null; michael@0: if (!aShouldBypassCache) { michael@0: try { michael@0: var imageCache = Components.classes["@mozilla.org/image/tools;1"] michael@0: .getService(Components.interfaces.imgITools) michael@0: .getImgCacheForDocument(aDoc); michael@0: var props = michael@0: imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null))); michael@0: if (props) { michael@0: contentType = props.get("type", nsISupportsCString); michael@0: contentDisposition = props.get("content-disposition", michael@0: nsISupportsCString); michael@0: } michael@0: } catch (e) { michael@0: // Failure to get type and content-disposition off the image is non-fatal michael@0: } michael@0: } michael@0: internalSave(aURL, null, aFileName, contentDisposition, contentType, michael@0: aShouldBypassCache, aFilePickerTitleKey, null, aReferrer, michael@0: aDoc, aSkipPrompt, null); michael@0: } michael@0: michael@0: function saveDocument(aDocument, aSkipPrompt) michael@0: { michael@0: if (!aDocument) michael@0: throw "Must have a document when calling saveDocument"; michael@0: michael@0: // We want to use cached data because the document is currently visible. michael@0: var ifreq = michael@0: aDocument.defaultView michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor); michael@0: michael@0: var contentDisposition = null; michael@0: try { michael@0: contentDisposition = michael@0: ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils) michael@0: .getDocumentMetadata("content-disposition"); michael@0: } catch (ex) { michael@0: // Failure to get a content-disposition is ok michael@0: } michael@0: michael@0: var cacheKey = null; michael@0: try { michael@0: cacheKey = michael@0: ifreq.getInterface(Components.interfaces.nsIWebNavigation) michael@0: .QueryInterface(Components.interfaces.nsIWebPageDescriptor); michael@0: } catch (ex) { michael@0: // We might not find it in the cache. Oh, well. michael@0: } michael@0: michael@0: internalSave(aDocument.location.href, aDocument, null, contentDisposition, michael@0: aDocument.contentType, false, null, null, michael@0: aDocument.referrer ? makeURI(aDocument.referrer) : null, michael@0: aDocument, aSkipPrompt, cacheKey); michael@0: } michael@0: michael@0: function DownloadListener(win, transfer) { michael@0: function makeClosure(name) { michael@0: return function() { michael@0: transfer[name].apply(transfer, arguments); michael@0: } michael@0: } michael@0: michael@0: this.window = win; michael@0: michael@0: // Now... we need to forward all calls to our transfer michael@0: for (var i in transfer) { michael@0: if (i != "QueryInterface") michael@0: this[i] = makeClosure(i); michael@0: } michael@0: } michael@0: michael@0: DownloadListener.prototype = { michael@0: QueryInterface: function dl_qi(aIID) michael@0: { michael@0: if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) || michael@0: aIID.equals(Components.interfaces.nsIWebProgressListener) || michael@0: aIID.equals(Components.interfaces.nsIWebProgressListener2) || michael@0: aIID.equals(Components.interfaces.nsISupports)) { michael@0: return this; michael@0: } michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: getInterface: function dl_gi(aIID) michael@0: { michael@0: if (aIID.equals(Components.interfaces.nsIAuthPrompt) || michael@0: aIID.equals(Components.interfaces.nsIAuthPrompt2)) { michael@0: var ww = michael@0: Components.classes["@mozilla.org/embedcomp/window-watcher;1"] michael@0: .getService(Components.interfaces.nsIPromptFactory); michael@0: return ww.getPrompt(this.window, aIID); michael@0: } michael@0: michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: } michael@0: michael@0: const kSaveAsType_Complete = 0; // Save document with attached objects. michael@0: // const kSaveAsType_URL = 1; // Save document or URL by itself. michael@0: const kSaveAsType_Text = 2; // Save document, converting to plain text. michael@0: michael@0: /** michael@0: * internalSave: Used when saving a document or URL. michael@0: * michael@0: * If aChosenData is null, this method: michael@0: * - Determines a local target filename to use michael@0: * - Prompts the user to confirm the destination filename and save mode michael@0: * (aContentType affects this) michael@0: * - [Note] This process involves the parameters aURL, aReferrer (to determine michael@0: * how aURL was encoded), aDocument, aDefaultFileName, aFilePickerTitleKey, michael@0: * and aSkipPrompt. michael@0: * michael@0: * If aChosenData is non-null, this method: michael@0: * - Uses the provided source URI and save file name michael@0: * - Saves the document as complete DOM if possible (aDocument present and michael@0: * right aContentType) michael@0: * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and michael@0: * aSkipPrompt are ignored. michael@0: * michael@0: * In any case, this method: michael@0: * - Creates a 'Persist' object (which will perform the saving in the michael@0: * background) and then starts it. michael@0: * - [Note] This part of the process only involves the parameters aDocument, michael@0: * aShouldBypassCache and aReferrer. The source, the save name and the save michael@0: * mode are the ones determined previously. michael@0: * michael@0: * @param aURL michael@0: * The String representation of the URL of the document being saved michael@0: * @param aDocument michael@0: * The document to be saved michael@0: * @param aDefaultFileName michael@0: * The caller-provided suggested filename if we don't michael@0: * find a better one michael@0: * @param aContentDisposition michael@0: * The caller-provided content-disposition header to use. michael@0: * @param aContentType michael@0: * The caller-provided content-type to use michael@0: * @param aShouldBypassCache michael@0: * If true, the document will always be refetched from the server michael@0: * @param aFilePickerTitleKey michael@0: * Alternate title for the file picker michael@0: * @param aChosenData michael@0: * If non-null this contains an instance of object AutoChosen (see below) michael@0: * which holds pre-determined data so that the user does not need to be michael@0: * prompted for a target filename. michael@0: * @param aReferrer michael@0: * the referrer URI object (not URL string) to use, or null michael@0: * if no referrer should be sent. michael@0: * @param aInitiatingDocument michael@0: * The document from which the save was initiated. michael@0: * @param aSkipPrompt [optional] michael@0: * If set to true, we will attempt to save the file to the michael@0: * default downloads folder without prompting. michael@0: * @param aCacheKey [optional] michael@0: * If set will be passed to saveURI. See nsIWebBrowserPersist for michael@0: * allowed values. michael@0: */ michael@0: function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, michael@0: aContentType, aShouldBypassCache, aFilePickerTitleKey, michael@0: aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt, michael@0: aCacheKey) michael@0: { michael@0: if (aSkipPrompt == undefined) michael@0: aSkipPrompt = false; michael@0: michael@0: if (aCacheKey == undefined) michael@0: aCacheKey = null; michael@0: michael@0: // Note: aDocument == null when this code is used by save-link-as... michael@0: var saveMode = GetSaveModeForContentType(aContentType, aDocument); michael@0: michael@0: var file, sourceURI, saveAsType; michael@0: // Find the URI object for aURL and the FileName/Extension to use when saving. michael@0: // FileName/Extension will be ignored if aChosenData supplied. michael@0: if (aChosenData) { michael@0: file = aChosenData.file; michael@0: sourceURI = aChosenData.uri; michael@0: saveAsType = kSaveAsType_Complete; michael@0: michael@0: continueSave(); michael@0: } else { michael@0: var charset = null; michael@0: if (aDocument) michael@0: charset = aDocument.characterSet; michael@0: else if (aReferrer) michael@0: charset = aReferrer.originCharset; michael@0: var fileInfo = new FileInfo(aDefaultFileName); michael@0: initFileInfo(fileInfo, aURL, charset, aDocument, michael@0: aContentType, aContentDisposition); michael@0: sourceURI = fileInfo.uri; michael@0: michael@0: var fpParams = { michael@0: fpTitleKey: aFilePickerTitleKey, michael@0: fileInfo: fileInfo, michael@0: contentType: aContentType, michael@0: saveMode: saveMode, michael@0: saveAsType: kSaveAsType_Complete, michael@0: file: file michael@0: }; michael@0: michael@0: // Find a URI to use for determining last-downloaded-to directory michael@0: let relatedURI = aReferrer || sourceURI; michael@0: michael@0: promiseTargetFile(fpParams, aSkipPrompt, relatedURI).then(aDialogAccepted => { michael@0: if (!aDialogAccepted) michael@0: return; michael@0: michael@0: saveAsType = fpParams.saveAsType; michael@0: file = fpParams.file; michael@0: michael@0: continueSave(); michael@0: }).then(null, Components.utils.reportError); michael@0: } michael@0: michael@0: function continueSave() { michael@0: // XXX We depend on the following holding true in appendFiltersForContentType(): michael@0: // If we should save as a complete page, the saveAsType is kSaveAsType_Complete. michael@0: // If we should save as text, the saveAsType is kSaveAsType_Text. michael@0: var useSaveDocument = aDocument && michael@0: (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) || michael@0: ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text))); michael@0: // If we're saving a document, and are saving either in complete mode or michael@0: // as converted text, pass the document to the web browser persist component. michael@0: // If we're just saving the HTML (second option in the list), send only the URI. michael@0: var persistArgs = { michael@0: sourceURI : sourceURI, michael@0: sourceReferrer : aReferrer, michael@0: sourceDocument : useSaveDocument ? aDocument : null, michael@0: targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null, michael@0: targetFile : file, michael@0: sourceCacheKey : aCacheKey, michael@0: sourcePostData : aDocument ? getPostData(aDocument) : null, michael@0: bypassCache : aShouldBypassCache, michael@0: initiatingWindow : aInitiatingDocument.defaultView michael@0: }; michael@0: michael@0: // Start the actual save process michael@0: internalPersist(persistArgs); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * internalPersist: Creates a 'Persist' object (which will perform the saving michael@0: * in the background) and then starts it. michael@0: * michael@0: * @param persistArgs.sourceURI michael@0: * The nsIURI of the document being saved michael@0: * @param persistArgs.sourceCacheKey [optional] michael@0: * If set will be passed to saveURI michael@0: * @param persistArgs.sourceDocument [optional] michael@0: * The document to be saved, or null if not saving a complete document michael@0: * @param persistArgs.sourceReferrer michael@0: * Required and used only when persistArgs.sourceDocument is NOT present, michael@0: * the nsIURI of the referrer to use, or null if no referrer should be michael@0: * sent. michael@0: * @param persistArgs.sourcePostData michael@0: * Required and used only when persistArgs.sourceDocument is NOT present, michael@0: * represents the POST data to be sent along with the HTTP request, and michael@0: * must be null if no POST data should be sent. michael@0: * @param persistArgs.targetFile michael@0: * The nsIFile of the file to create michael@0: * @param persistArgs.targetContentType michael@0: * Required and used only when persistArgs.sourceDocument is present, michael@0: * determines the final content type of the saved file, or null to use michael@0: * the same content type as the source document. Currently only michael@0: * "text/plain" is meaningful. michael@0: * @param persistArgs.bypassCache michael@0: * If true, the document will always be refetched from the server michael@0: * @param persistArgs.initiatingWindow michael@0: * The window from which the save operation was initiated. michael@0: */ michael@0: function internalPersist(persistArgs) michael@0: { michael@0: var persist = makeWebBrowserPersist(); michael@0: michael@0: // Calculate persist flags. michael@0: const nsIWBP = Components.interfaces.nsIWebBrowserPersist; michael@0: const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES | michael@0: nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES; michael@0: if (persistArgs.bypassCache) michael@0: persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE; michael@0: else michael@0: persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE; michael@0: michael@0: // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof): michael@0: persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; michael@0: michael@0: // Find the URI associated with the target file michael@0: var targetFileURL = makeFileURI(persistArgs.targetFile); michael@0: michael@0: var isPrivate = PrivateBrowsingUtils.isWindowPrivate(persistArgs.initiatingWindow); michael@0: michael@0: // Create download and initiate it (below) michael@0: var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer); michael@0: tr.init(persistArgs.sourceURI, michael@0: targetFileURL, "", null, null, null, persist, isPrivate); michael@0: persist.progressListener = new DownloadListener(window, tr); michael@0: michael@0: if (persistArgs.sourceDocument) { michael@0: // Saving a Document, not a URI: michael@0: var filesFolder = null; michael@0: if (persistArgs.targetContentType != "text/plain") { michael@0: // Create the local directory into which to save associated files. michael@0: filesFolder = persistArgs.targetFile.clone(); michael@0: michael@0: var nameWithoutExtension = getFileBaseName(filesFolder.leafName); michael@0: var filesFolderLeafName = michael@0: ContentAreaUtils.stringBundle michael@0: .formatStringFromName("filesFolder", [nameWithoutExtension], 1); michael@0: michael@0: filesFolder.leafName = filesFolderLeafName; michael@0: } michael@0: michael@0: var encodingFlags = 0; michael@0: if (persistArgs.targetContentType == "text/plain") { michael@0: encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED; michael@0: encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; michael@0: encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; michael@0: } michael@0: else { michael@0: encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; michael@0: } michael@0: michael@0: const kWrapColumn = 80; michael@0: persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder, michael@0: persistArgs.targetContentType, encodingFlags, kWrapColumn); michael@0: } else { michael@0: let privacyContext = persistArgs.initiatingWindow michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIWebNavigation) michael@0: .QueryInterface(Components.interfaces.nsILoadContext); michael@0: persist.saveURI(persistArgs.sourceURI, michael@0: persistArgs.sourceCacheKey, persistArgs.sourceReferrer, persistArgs.sourcePostData, null, michael@0: targetFileURL, privacyContext); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Structure for holding info about automatically supplied parameters for michael@0: * internalSave(...). This allows parameters to be supplied so the user does not michael@0: * need to be prompted for file info. michael@0: * @param aFileAutoChosen This is an nsIFile object that has been michael@0: * pre-determined as the filename for the target to save to michael@0: * @param aUriAutoChosen This is the nsIURI object for the target michael@0: */ michael@0: function AutoChosen(aFileAutoChosen, aUriAutoChosen) { michael@0: this.file = aFileAutoChosen; michael@0: this.uri = aUriAutoChosen; michael@0: } michael@0: michael@0: /** michael@0: * Structure for holding info about a URL and the target filename it should be michael@0: * saved to. This structure is populated by initFileInfo(...). michael@0: * @param aSuggestedFileName This is used by initFileInfo(...) when it michael@0: * cannot 'discover' the filename from the url michael@0: * @param aFileName The target filename michael@0: * @param aFileBaseName The filename without the file extension michael@0: * @param aFileExt The extension of the filename michael@0: * @param aUri An nsIURI object for the url that is being saved michael@0: */ michael@0: function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) { michael@0: this.suggestedFileName = aSuggestedFileName; michael@0: this.fileName = aFileName; michael@0: this.fileBaseName = aFileBaseName; michael@0: this.fileExt = aFileExt; michael@0: this.uri = aUri; michael@0: } michael@0: michael@0: /** michael@0: * Determine what the 'default' filename string is, its file extension and the michael@0: * filename without the extension. This filename is used when prompting the user michael@0: * for confirmation in the file picker dialog. michael@0: * @param aFI A FileInfo structure into which we'll put the results of this method. michael@0: * @param aURL The String representation of the URL of the document being saved michael@0: * @param aURLCharset The charset of aURL. michael@0: * @param aDocument The document to be saved michael@0: * @param aContentType The content type we're saving, if it could be michael@0: * determined by the caller. michael@0: * @param aContentDisposition The content-disposition header for the object michael@0: * we're saving, if it could be determined by the caller. michael@0: */ michael@0: function initFileInfo(aFI, aURL, aURLCharset, aDocument, michael@0: aContentType, aContentDisposition) michael@0: { michael@0: try { michael@0: // Get an nsIURI object from aURL if possible: michael@0: try { michael@0: aFI.uri = makeURI(aURL, aURLCharset); michael@0: // Assuming nsiUri is valid, calling QueryInterface(...) on it will michael@0: // populate extra object fields (eg filename and file extension). michael@0: var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL); michael@0: aFI.fileExt = url.fileExtension; michael@0: } catch (e) { michael@0: } michael@0: michael@0: // Get the default filename: michael@0: aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName), michael@0: aFI.uri, aDocument, aContentDisposition); michael@0: // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied michael@0: // if saveURL(...) was the original caller (hence both aContentType and michael@0: // aDocument are blank). If they were saving a link to a website then make michael@0: // the extension .htm . michael@0: if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) { michael@0: aFI.fileExt = "htm"; michael@0: aFI.fileBaseName = aFI.fileName; michael@0: } else { michael@0: aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType); michael@0: aFI.fileBaseName = getFileBaseName(aFI.fileName); michael@0: } michael@0: } catch (e) { michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Given the Filepicker Parameters (aFpP), show the file picker dialog, michael@0: * prompting the user to confirm (or change) the fileName. michael@0: * @param aFpP michael@0: * A structure (see definition in internalSave(...) method) michael@0: * containing all the data used within this method. michael@0: * @param aSkipPrompt michael@0: * If true, attempt to save the file automatically to the user's default michael@0: * download directory, thus skipping the explicit prompt for a file name, michael@0: * but only if the associated preference is set. michael@0: * If false, don't save the file automatically to the user's michael@0: * default download directory, even if the associated preference michael@0: * is set, but ask for the target explicitly. michael@0: * @param aRelatedURI michael@0: * An nsIURI associated with the download. The last used michael@0: * directory of the picker is retrieved from/stored in the michael@0: * Content Pref Service using this URI. michael@0: * @return Promise michael@0: * @resolve a boolean. When true, it indicates that the file picker dialog michael@0: * is accepted. michael@0: */ michael@0: function promiseTargetFile(aFpP, /* optional */ aSkipPrompt, /* optional */ aRelatedURI) michael@0: { michael@0: return Task.spawn(function() { michael@0: let downloadLastDir = new DownloadLastDir(window); michael@0: let prefBranch = Services.prefs.getBranch("browser.download."); michael@0: let useDownloadDir = prefBranch.getBoolPref("useDownloadDir"); michael@0: michael@0: if (!aSkipPrompt) michael@0: useDownloadDir = false; michael@0: michael@0: // Default to the user's default downloads directory configured michael@0: // through download prefs. michael@0: let dirPath = yield Downloads.getPreferredDownloadsDirectory(); michael@0: let dirExists = yield OS.File.exists(dirPath); michael@0: let dir = new FileUtils.File(dirPath); michael@0: michael@0: if (useDownloadDir && dirExists) { michael@0: dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName, michael@0: aFpP.fileInfo.fileExt)); michael@0: aFpP.file = uniqueFile(dir); michael@0: throw new Task.Result(true); michael@0: } michael@0: michael@0: // We must prompt for the file name explicitly. michael@0: // If we must prompt because we were asked to... michael@0: let deferred = Promise.defer(); michael@0: if (useDownloadDir) { michael@0: // Keep async behavior in both branches michael@0: Services.tm.mainThread.dispatch(function() { michael@0: deferred.resolve(null); michael@0: }, Components.interfaces.nsIThread.DISPATCH_NORMAL); michael@0: } else { michael@0: downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(aFile) { michael@0: deferred.resolve(aFile); michael@0: }); michael@0: } michael@0: let file = yield deferred.promise; michael@0: if (file && (yield OS.File.exists(file.path))) { michael@0: dir = file; michael@0: dirExists = true; michael@0: } michael@0: michael@0: if (!dirExists) { michael@0: // Default to desktop. michael@0: dir = Services.dirsvc.get("Desk", Components.interfaces.nsIFile); michael@0: } michael@0: michael@0: let fp = makeFilePicker(); michael@0: let titleKey = aFpP.fpTitleKey || "SaveLinkTitle"; michael@0: fp.init(window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey), michael@0: Components.interfaces.nsIFilePicker.modeSave); michael@0: michael@0: fp.displayDirectory = dir; michael@0: fp.defaultExtension = aFpP.fileInfo.fileExt; michael@0: fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName, michael@0: aFpP.fileInfo.fileExt); michael@0: appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt, michael@0: aFpP.saveMode); michael@0: michael@0: // The index of the selected filter is only preserved and restored if there's michael@0: // more than one filter in addition to "All Files". michael@0: if (aFpP.saveMode != SAVEMODE_FILEONLY) { michael@0: try { michael@0: fp.filterIndex = prefBranch.getIntPref("save_converter_index"); michael@0: } michael@0: catch (e) { michael@0: } michael@0: } michael@0: michael@0: let deferComplete = Promise.defer(); michael@0: fp.open(function(aResult) { michael@0: deferComplete.resolve(aResult); michael@0: }); michael@0: let result = yield deferComplete.promise; michael@0: if (result == Components.interfaces.nsIFilePicker.returnCancel || !fp.file) { michael@0: throw new Task.Result(false); michael@0: } michael@0: michael@0: if (aFpP.saveMode != SAVEMODE_FILEONLY) michael@0: prefBranch.setIntPref("save_converter_index", fp.filterIndex); michael@0: michael@0: // Do not store the last save directory as a pref inside the private browsing mode michael@0: downloadLastDir.setFile(aRelatedURI, fp.file.parent); michael@0: michael@0: fp.file.leafName = validateFileName(fp.file.leafName); michael@0: michael@0: aFpP.saveAsType = fp.filterIndex; michael@0: aFpP.file = fp.file; michael@0: aFpP.fileURL = fp.fileURL; michael@0: michael@0: throw new Task.Result(true); michael@0: }); michael@0: } michael@0: michael@0: // Since we're automatically downloading, we don't get the file picker's michael@0: // logic to check for existing files, so we need to do that here. michael@0: // michael@0: // Note - this code is identical to that in michael@0: // mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in michael@0: // If you are updating this code, update that code too! We can't share code michael@0: // here since that code is called in a js component. michael@0: function uniqueFile(aLocalFile) michael@0: { michael@0: var collisionCount = 0; michael@0: while (aLocalFile.exists()) { michael@0: collisionCount++; michael@0: if (collisionCount == 1) { michael@0: // Append "(2)" before the last dot in (or at the end of) the filename michael@0: // special case .ext.gz etc files so we don't wind up with .tar(2).gz michael@0: if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) michael@0: aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); michael@0: else michael@0: aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); michael@0: } michael@0: else { michael@0: // replace the last (n) in the filename with (n+1) michael@0: aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")"); michael@0: } michael@0: } michael@0: return aLocalFile; michael@0: } michael@0: michael@0: #ifdef MOZ_JSDOWNLOADS michael@0: /** michael@0: * Download a URL using the new jsdownloads API. michael@0: * michael@0: * @param aURL michael@0: * the url to download michael@0: * @param [optional] aFileName michael@0: * the destination file name, if omitted will be obtained from the url. michael@0: * @param aInitiatingDocument michael@0: * The document from which the download was initiated. michael@0: */ michael@0: function DownloadURL(aURL, aFileName, aInitiatingDocument) { michael@0: // For private browsing, try to get document out of the most recent browser michael@0: // window, or provide our own if there's no browser window. michael@0: let isPrivate = aInitiatingDocument.defaultView michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsILoadContext) michael@0: .usePrivateBrowsing; michael@0: michael@0: let fileInfo = new FileInfo(aFileName); michael@0: initFileInfo(fileInfo, aURL, null, null, null, null); michael@0: michael@0: let filepickerParams = { michael@0: fileInfo: fileInfo, michael@0: saveMode: SAVEMODE_FILEONLY michael@0: }; michael@0: michael@0: Task.spawn(function* () { michael@0: let accepted = yield promiseTargetFile(filepickerParams, true, fileInfo.uri); michael@0: if (!accepted) michael@0: return; michael@0: michael@0: let file = filepickerParams.file; michael@0: let download = yield Downloads.createDownload({ michael@0: source: { url: aURL, isPrivate: isPrivate }, michael@0: target: { path: file.path, partFilePath: file.path + ".part" } michael@0: }); michael@0: download.tryToKeepPartialData = true; michael@0: download.start(); michael@0: michael@0: // Add the download to the list, allowing it to be managed. michael@0: let list = yield Downloads.getList(Downloads.ALL); michael@0: list.add(download); michael@0: }).then(null, Components.utils.reportError); michael@0: } michael@0: #endif michael@0: michael@0: // We have no DOM, and can only save the URL as is. michael@0: const SAVEMODE_FILEONLY = 0x00; michael@0: // We have a DOM and can save as complete. michael@0: const SAVEMODE_COMPLETE_DOM = 0x01; michael@0: // We have a DOM which we can serialize as text. michael@0: const SAVEMODE_COMPLETE_TEXT = 0x02; michael@0: michael@0: // If we are able to save a complete DOM, the 'save as complete' filter michael@0: // must be the first filter appended. The 'save page only' counterpart michael@0: // must be the second filter appended. And the 'save as complete text' michael@0: // filter must be the third filter appended. michael@0: function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode) michael@0: { michael@0: // The bundle name for saving only a specific content type. michael@0: var bundleName; michael@0: // The corresponding filter string for a specific content type. michael@0: var filterString; michael@0: michael@0: // XXX all the cases that are handled explicitly here MUST be handled michael@0: // in GetSaveModeForContentType to return a non-fileonly filter. michael@0: switch (aContentType) { michael@0: case "text/html": michael@0: bundleName = "WebPageHTMLOnlyFilter"; michael@0: filterString = "*.htm; *.html"; michael@0: break; michael@0: michael@0: case "application/xhtml+xml": michael@0: bundleName = "WebPageXHTMLOnlyFilter"; michael@0: filterString = "*.xht; *.xhtml"; michael@0: break; michael@0: michael@0: case "image/svg+xml": michael@0: bundleName = "WebPageSVGOnlyFilter"; michael@0: filterString = "*.svg; *.svgz"; michael@0: break; michael@0: michael@0: case "text/xml": michael@0: case "application/xml": michael@0: bundleName = "WebPageXMLOnlyFilter"; michael@0: filterString = "*.xml"; michael@0: break; michael@0: michael@0: default: michael@0: if (aSaveMode != SAVEMODE_FILEONLY) michael@0: throw "Invalid save mode for type '" + aContentType + "'"; michael@0: michael@0: var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension); michael@0: if (mimeInfo) { michael@0: michael@0: var extEnumerator = mimeInfo.getFileExtensions(); michael@0: michael@0: var extString = ""; michael@0: while (extEnumerator.hasMore()) { michael@0: var extension = extEnumerator.getNext(); michael@0: if (extString) michael@0: extString += "; "; // If adding more than one extension, michael@0: // separate by semi-colon michael@0: extString += "*." + extension; michael@0: } michael@0: michael@0: if (extString) michael@0: aFilePicker.appendFilter(mimeInfo.description, extString); michael@0: } michael@0: michael@0: break; michael@0: } michael@0: michael@0: if (aSaveMode & SAVEMODE_COMPLETE_DOM) { michael@0: aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"), michael@0: filterString); michael@0: // We should always offer a choice to save document only if michael@0: // we allow saving as complete. michael@0: aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName(bundleName), michael@0: filterString); michael@0: } michael@0: michael@0: if (aSaveMode & SAVEMODE_COMPLETE_TEXT) michael@0: aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText); michael@0: michael@0: // Always append the all files (*) filter michael@0: aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll); michael@0: } michael@0: michael@0: function getPostData(aDocument) michael@0: { michael@0: try { michael@0: // Find the session history entry corresponding to the given document. In michael@0: // the current implementation, nsIWebPageDescriptor.currentDescriptor always michael@0: // returns a session history entry. michael@0: var sessionHistoryEntry = michael@0: aDocument.defaultView michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIWebNavigation) michael@0: .QueryInterface(Components.interfaces.nsIWebPageDescriptor) michael@0: .currentDescriptor michael@0: .QueryInterface(Components.interfaces.nsISHEntry); michael@0: return sessionHistoryEntry.postData; michael@0: } michael@0: catch (e) { michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: function makeWebBrowserPersist() michael@0: { michael@0: const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"; michael@0: const persistIID = Components.interfaces.nsIWebBrowserPersist; michael@0: return Components.classes[persistContractID].createInstance(persistIID); michael@0: } michael@0: michael@0: function makeURI(aURL, aOriginCharset, aBaseURI) michael@0: { michael@0: return BrowserUtils.makeURI(aURL, aOriginCharset, aBaseURI); michael@0: } michael@0: michael@0: function makeFileURI(aFile) michael@0: { michael@0: return BrowserUtils.makeFileURI(aFile); michael@0: } michael@0: michael@0: function makeFilePicker() michael@0: { michael@0: const fpContractID = "@mozilla.org/filepicker;1"; michael@0: const fpIID = Components.interfaces.nsIFilePicker; michael@0: return Components.classes[fpContractID].createInstance(fpIID); michael@0: } michael@0: michael@0: function getMIMEService() michael@0: { michael@0: const mimeSvcContractID = "@mozilla.org/mime;1"; michael@0: const mimeSvcIID = Components.interfaces.nsIMIMEService; michael@0: const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID); michael@0: return mimeSvc; michael@0: } michael@0: michael@0: // Given aFileName, find the fileName without the extension on the end. michael@0: function getFileBaseName(aFileName) michael@0: { michael@0: // Remove the file extension from aFileName: michael@0: return aFileName.replace(/\.[^.]*$/, ""); michael@0: } michael@0: michael@0: function getMIMETypeForURI(aURI) michael@0: { michael@0: try { michael@0: return getMIMEService().getTypeFromURI(aURI); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: function getMIMEInfoForType(aMIMEType, aExtension) michael@0: { michael@0: if (aMIMEType || aExtension) { michael@0: try { michael@0: return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension); michael@0: } michael@0: catch (e) { michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: function getDefaultFileName(aDefaultFileName, aURI, aDocument, michael@0: aContentDisposition) michael@0: { michael@0: // 1) look for a filename in the content-disposition header, if any michael@0: if (aContentDisposition) { michael@0: const mhpContractID = "@mozilla.org/network/mime-hdrparam;1"; michael@0: const mhpIID = Components.interfaces.nsIMIMEHeaderParam; michael@0: const mhp = Components.classes[mhpContractID].getService(mhpIID); michael@0: var dummy = { value: null }; // Need an out param... michael@0: var charset = getCharsetforSave(aDocument); michael@0: michael@0: var fileName = null; michael@0: try { michael@0: fileName = mhp.getParameter(aContentDisposition, "filename", charset, michael@0: true, dummy); michael@0: } michael@0: catch (e) { michael@0: try { michael@0: fileName = mhp.getParameter(aContentDisposition, "name", charset, true, michael@0: dummy); michael@0: } michael@0: catch (e) { michael@0: } michael@0: } michael@0: if (fileName) michael@0: return fileName; michael@0: } michael@0: michael@0: let docTitle; michael@0: if (aDocument) { michael@0: // If the document looks like HTML or XML, try to use its original title. michael@0: docTitle = validateFileName(aDocument.title).trim(); michael@0: if (docTitle) { michael@0: let contentType = aDocument.contentType; michael@0: if (contentType == "application/xhtml+xml" || michael@0: contentType == "application/xml" || michael@0: contentType == "image/svg+xml" || michael@0: contentType == "text/html" || michael@0: contentType == "text/xml") { michael@0: // 2) Use the document title michael@0: return docTitle; michael@0: } michael@0: } michael@0: } michael@0: michael@0: try { michael@0: var url = aURI.QueryInterface(Components.interfaces.nsIURL); michael@0: if (url.fileName != "") { michael@0: // 3) Use the actual file name, if present michael@0: var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"] michael@0: .getService(Components.interfaces.nsITextToSubURI); michael@0: return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName)); michael@0: } michael@0: } catch (e) { michael@0: // This is something like a data: and so forth URI... no filename here. michael@0: } michael@0: michael@0: if (docTitle) michael@0: // 4) Use the document title michael@0: return docTitle; michael@0: michael@0: if (aDefaultFileName) michael@0: // 5) Use the caller-provided name, if any michael@0: return validateFileName(aDefaultFileName); michael@0: michael@0: // 6) If this is a directory, use the last directory name michael@0: var path = aURI.path.match(/\/([^\/]+)\/$/); michael@0: if (path && path.length > 1) michael@0: return validateFileName(path[1]); michael@0: michael@0: try { michael@0: if (aURI.host) michael@0: // 7) Use the host. michael@0: return aURI.host; michael@0: } catch (e) { michael@0: // Some files have no information at all, like Javascript generated pages michael@0: } michael@0: try { michael@0: // 8) Use the default file name michael@0: return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName"); michael@0: } catch (e) { michael@0: //in case localized string cannot be found michael@0: } michael@0: // 9) If all else fails, use "index" michael@0: return "index"; michael@0: } michael@0: michael@0: function validateFileName(aFileName) michael@0: { michael@0: var re = /[\/]+/g; michael@0: if (navigator.appVersion.indexOf("Windows") != -1) { michael@0: re = /[\\\/\|]+/g; michael@0: aFileName = aFileName.replace(/[\"]+/g, "'"); michael@0: aFileName = aFileName.replace(/[\*\:\?]+/g, " "); michael@0: aFileName = aFileName.replace(/[\<]+/g, "("); michael@0: aFileName = aFileName.replace(/[\>]+/g, ")"); michael@0: } michael@0: else if (navigator.appVersion.indexOf("Macintosh") != -1) michael@0: re = /[\:\/]+/g; michael@0: else if (navigator.appVersion.indexOf("Android") != -1) { michael@0: // On mobile devices, the filesystem may be very limited in what michael@0: // it considers valid characters. To avoid errors, we sanitize michael@0: // conservatively. michael@0: const dangerousChars = "*?<>|\":/\\[];,+="; michael@0: var processed = ""; michael@0: for (var i = 0; i < aFileName.length; i++) michael@0: processed += aFileName.charCodeAt(i) >= 32 && michael@0: !(dangerousChars.indexOf(aFileName[i]) >= 0) ? aFileName[i] michael@0: : "_"; michael@0: michael@0: // Last character should not be a space michael@0: processed = processed.trim(); michael@0: michael@0: // If a large part of the filename has been sanitized, then we michael@0: // will use a default filename instead michael@0: if (processed.replace(/_/g, "").length <= processed.length/2) { michael@0: // We purposefully do not use a localized default filename, michael@0: // which we could have done using michael@0: // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName") michael@0: // since it may contain invalid characters. michael@0: var original = processed; michael@0: processed = "download"; michael@0: michael@0: // Preserve a suffix, if there is one michael@0: if (original.indexOf(".") >= 0) { michael@0: var suffix = original.split(".").slice(-1)[0]; michael@0: if (suffix && suffix.indexOf("_") < 0) michael@0: processed += "." + suffix; michael@0: } michael@0: } michael@0: return processed; michael@0: } michael@0: michael@0: return aFileName.replace(re, "_"); michael@0: } michael@0: michael@0: function getNormalizedLeafName(aFile, aDefaultExtension) michael@0: { michael@0: if (!aDefaultExtension) michael@0: return aFile; michael@0: michael@0: #ifdef XP_WIN michael@0: // Remove trailing dots and spaces on windows michael@0: aFile = aFile.replace(/[\s.]+$/, ""); michael@0: #endif michael@0: michael@0: // Remove leading dots michael@0: aFile = aFile.replace(/^\.+/, ""); michael@0: michael@0: // Fix up the file name we're saving to to include the default extension michael@0: var i = aFile.lastIndexOf("."); michael@0: if (aFile.substr(i + 1) != aDefaultExtension) michael@0: return aFile + "." + aDefaultExtension; michael@0: michael@0: return aFile; michael@0: } michael@0: michael@0: function getDefaultExtension(aFilename, aURI, aContentType) michael@0: { michael@0: if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp") michael@0: return ""; // temporary fix for bug 120327 michael@0: michael@0: // First try the extension from the filename michael@0: const stdURLContractID = "@mozilla.org/network/standard-url;1"; michael@0: const stdURLIID = Components.interfaces.nsIURL; michael@0: var url = Components.classes[stdURLContractID].createInstance(stdURLIID); michael@0: url.filePath = aFilename; michael@0: michael@0: var ext = url.fileExtension; michael@0: michael@0: // This mirrors some code in nsExternalHelperAppService::DoContent michael@0: // Use the filename first and then the URI if that fails michael@0: michael@0: var mimeInfo = getMIMEInfoForType(aContentType, ext); michael@0: michael@0: if (ext && mimeInfo && mimeInfo.extensionExists(ext)) michael@0: return ext; michael@0: michael@0: // Well, that failed. Now try the extension from the URI michael@0: var urlext; michael@0: try { michael@0: url = aURI.QueryInterface(Components.interfaces.nsIURL); michael@0: urlext = url.fileExtension; michael@0: } catch (e) { michael@0: } michael@0: michael@0: if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) { michael@0: return urlext; michael@0: } michael@0: else { michael@0: try { michael@0: if (mimeInfo) michael@0: return mimeInfo.primaryExtension; michael@0: } michael@0: catch (e) { michael@0: } michael@0: // Fall back on the extensions in the filename and URI for lack michael@0: // of anything better. michael@0: return ext || urlext; michael@0: } michael@0: } michael@0: michael@0: function GetSaveModeForContentType(aContentType, aDocument) michael@0: { michael@0: // We can only save a complete page if we have a loaded document michael@0: if (!aDocument) michael@0: return SAVEMODE_FILEONLY; michael@0: michael@0: // Find the possible save modes using the provided content type michael@0: var saveMode = SAVEMODE_FILEONLY; michael@0: switch (aContentType) { michael@0: case "text/html": michael@0: case "application/xhtml+xml": michael@0: case "image/svg+xml": michael@0: saveMode |= SAVEMODE_COMPLETE_TEXT; michael@0: // Fall through michael@0: case "text/xml": michael@0: case "application/xml": michael@0: saveMode |= SAVEMODE_COMPLETE_DOM; michael@0: break; michael@0: } michael@0: michael@0: return saveMode; michael@0: } michael@0: michael@0: function getCharsetforSave(aDocument) michael@0: { michael@0: if (aDocument) michael@0: return aDocument.characterSet; michael@0: michael@0: if (document.commandDispatcher.focusedWindow) michael@0: return document.commandDispatcher.focusedWindow.document.characterSet; michael@0: michael@0: return window.content.document.characterSet; michael@0: } michael@0: michael@0: /** michael@0: * Open a URL from chrome, determining if we can handle it internally or need to michael@0: * launch an external application to handle it. michael@0: * @param aURL The URL to be opened michael@0: */ michael@0: function openURL(aURL) michael@0: { michael@0: var uri = makeURI(aURL); michael@0: michael@0: var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"] michael@0: .getService(Components.interfaces.nsIExternalProtocolService); michael@0: michael@0: if (!protocolSvc.isExposedProtocol(uri.scheme)) { michael@0: // If we're not a browser, use the external protocol service to load the URI. michael@0: protocolSvc.loadUrl(uri); michael@0: } michael@0: else { michael@0: var recentWindow = Services.wm.getMostRecentWindow("navigator:browser"); michael@0: if (recentWindow) { michael@0: recentWindow.openUILinkIn(uri.spec, "tab"); michael@0: return; michael@0: } michael@0: michael@0: var loadgroup = Components.classes["@mozilla.org/network/load-group;1"] michael@0: .createInstance(Components.interfaces.nsILoadGroup); michael@0: var appstartup = Services.startup; michael@0: michael@0: var loadListener = { michael@0: onStartRequest: function ll_start(aRequest, aContext) { michael@0: appstartup.enterLastWindowClosingSurvivalArea(); michael@0: }, michael@0: onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) { michael@0: appstartup.exitLastWindowClosingSurvivalArea(); michael@0: }, michael@0: QueryInterface: function ll_QI(iid) { michael@0: if (iid.equals(Components.interfaces.nsISupports) || michael@0: iid.equals(Components.interfaces.nsIRequestObserver) || michael@0: iid.equals(Components.interfaces.nsISupportsWeakReference)) michael@0: return this; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: } michael@0: loadgroup.groupObserver = loadListener; michael@0: michael@0: var uriListener = { michael@0: onStartURIOpen: function(uri) { return false; }, michael@0: doContent: function(ctype, preferred, request, handler) { return false; }, michael@0: isPreferred: function(ctype, desired) { return false; }, michael@0: canHandleContent: function(ctype, preferred, desired) { return false; }, michael@0: loadCookie: null, michael@0: parentContentListener: null, michael@0: getInterface: function(iid) { michael@0: if (iid.equals(Components.interfaces.nsIURIContentListener)) michael@0: return this; michael@0: if (iid.equals(Components.interfaces.nsILoadGroup)) michael@0: return loadgroup; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: } michael@0: michael@0: var channel = Services.io.newChannelFromURI(uri); michael@0: var uriLoader = Components.classes["@mozilla.org/uriloader;1"] michael@0: .getService(Components.interfaces.nsIURILoader); michael@0: uriLoader.openURI(channel, michael@0: Components.interfaces.nsIURILoader.IS_CONTENT_PREFERRED, michael@0: uriListener); michael@0: } michael@0: }