1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/content/contentAreaUtils.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1165 @@ 1.4 +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 1.5 +# This Source Code Form is subject to the terms of the Mozilla Public 1.6 +# License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.8 + 1.9 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.10 + 1.11 +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", 1.12 + "resource://gre/modules/BrowserUtils.jsm"); 1.13 +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", 1.14 + "resource://gre/modules/Downloads.jsm"); 1.15 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadLastDir", 1.16 + "resource://gre/modules/DownloadLastDir.jsm"); 1.17 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 1.18 + "resource://gre/modules/FileUtils.jsm"); 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.20 + "resource://gre/modules/osfile.jsm"); 1.21 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", 1.22 + "resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.24 + "resource://gre/modules/Promise.jsm"); 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "Services", 1.26 + "resource://gre/modules/Services.jsm"); 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.28 + "resource://gre/modules/Task.jsm"); 1.29 +var ContentAreaUtils = { 1.30 + 1.31 + // this is for backwards compatibility. 1.32 + get ioService() { 1.33 + return Services.io; 1.34 + }, 1.35 + 1.36 + get stringBundle() { 1.37 + delete this.stringBundle; 1.38 + return this.stringBundle = 1.39 + Services.strings.createBundle("chrome://global/locale/contentAreaCommands.properties"); 1.40 + } 1.41 +} 1.42 + 1.43 +function urlSecurityCheck(aURL, aPrincipal, aFlags) 1.44 +{ 1.45 + return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags); 1.46 +} 1.47 + 1.48 +/** 1.49 + * Determine whether or not a given focused DOMWindow is in the content area. 1.50 + **/ 1.51 +function isContentFrame(aFocusedWindow) 1.52 +{ 1.53 + if (!aFocusedWindow) 1.54 + return false; 1.55 + 1.56 + return (aFocusedWindow.top == window.content); 1.57 +} 1.58 + 1.59 +// Clientele: (Make sure you don't break any of these) 1.60 +// - File -> Save Page/Frame As... 1.61 +// - Context -> Save Page/Frame As... 1.62 +// - Context -> Save Link As... 1.63 +// - Alt-Click links in web pages 1.64 +// - Alt-Click links in the UI 1.65 +// 1.66 +// Try saving each of these types: 1.67 +// - A complete webpage using File->Save Page As, and Context->Save Page As 1.68 +// - A webpage as HTML only using the above methods 1.69 +// - A webpage as Text only using the above methods 1.70 +// - An image with an extension (e.g. .jpg) in its file name, using 1.71 +// Context->Save Image As... 1.72 +// - An image without an extension (e.g. a banner ad on cnn.com) using 1.73 +// the above method. 1.74 +// - A linked document using Save Link As... 1.75 +// - A linked document using Alt-click Save Link As... 1.76 +// 1.77 +function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, 1.78 + aSkipPrompt, aReferrer, aSourceDocument) 1.79 +{ 1.80 + internalSave(aURL, null, aFileName, null, null, aShouldBypassCache, 1.81 + aFilePickerTitleKey, null, aReferrer, aSourceDocument, 1.82 + aSkipPrompt, null); 1.83 +} 1.84 + 1.85 +// Just like saveURL, but will get some info off the image before 1.86 +// calling internalSave 1.87 +// Clientele: (Make sure you don't break any of these) 1.88 +// - Context -> Save Image As... 1.89 +const imgICache = Components.interfaces.imgICache; 1.90 +const nsISupportsCString = Components.interfaces.nsISupportsCString; 1.91 + 1.92 +function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, 1.93 + aSkipPrompt, aReferrer, aDoc) 1.94 +{ 1.95 + var contentType = null; 1.96 + var contentDisposition = null; 1.97 + if (!aShouldBypassCache) { 1.98 + try { 1.99 + var imageCache = Components.classes["@mozilla.org/image/tools;1"] 1.100 + .getService(Components.interfaces.imgITools) 1.101 + .getImgCacheForDocument(aDoc); 1.102 + var props = 1.103 + imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null))); 1.104 + if (props) { 1.105 + contentType = props.get("type", nsISupportsCString); 1.106 + contentDisposition = props.get("content-disposition", 1.107 + nsISupportsCString); 1.108 + } 1.109 + } catch (e) { 1.110 + // Failure to get type and content-disposition off the image is non-fatal 1.111 + } 1.112 + } 1.113 + internalSave(aURL, null, aFileName, contentDisposition, contentType, 1.114 + aShouldBypassCache, aFilePickerTitleKey, null, aReferrer, 1.115 + aDoc, aSkipPrompt, null); 1.116 +} 1.117 + 1.118 +function saveDocument(aDocument, aSkipPrompt) 1.119 +{ 1.120 + if (!aDocument) 1.121 + throw "Must have a document when calling saveDocument"; 1.122 + 1.123 + // We want to use cached data because the document is currently visible. 1.124 + var ifreq = 1.125 + aDocument.defaultView 1.126 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor); 1.127 + 1.128 + var contentDisposition = null; 1.129 + try { 1.130 + contentDisposition = 1.131 + ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils) 1.132 + .getDocumentMetadata("content-disposition"); 1.133 + } catch (ex) { 1.134 + // Failure to get a content-disposition is ok 1.135 + } 1.136 + 1.137 + var cacheKey = null; 1.138 + try { 1.139 + cacheKey = 1.140 + ifreq.getInterface(Components.interfaces.nsIWebNavigation) 1.141 + .QueryInterface(Components.interfaces.nsIWebPageDescriptor); 1.142 + } catch (ex) { 1.143 + // We might not find it in the cache. Oh, well. 1.144 + } 1.145 + 1.146 + internalSave(aDocument.location.href, aDocument, null, contentDisposition, 1.147 + aDocument.contentType, false, null, null, 1.148 + aDocument.referrer ? makeURI(aDocument.referrer) : null, 1.149 + aDocument, aSkipPrompt, cacheKey); 1.150 +} 1.151 + 1.152 +function DownloadListener(win, transfer) { 1.153 + function makeClosure(name) { 1.154 + return function() { 1.155 + transfer[name].apply(transfer, arguments); 1.156 + } 1.157 + } 1.158 + 1.159 + this.window = win; 1.160 + 1.161 + // Now... we need to forward all calls to our transfer 1.162 + for (var i in transfer) { 1.163 + if (i != "QueryInterface") 1.164 + this[i] = makeClosure(i); 1.165 + } 1.166 +} 1.167 + 1.168 +DownloadListener.prototype = { 1.169 + QueryInterface: function dl_qi(aIID) 1.170 + { 1.171 + if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) || 1.172 + aIID.equals(Components.interfaces.nsIWebProgressListener) || 1.173 + aIID.equals(Components.interfaces.nsIWebProgressListener2) || 1.174 + aIID.equals(Components.interfaces.nsISupports)) { 1.175 + return this; 1.176 + } 1.177 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.178 + }, 1.179 + 1.180 + getInterface: function dl_gi(aIID) 1.181 + { 1.182 + if (aIID.equals(Components.interfaces.nsIAuthPrompt) || 1.183 + aIID.equals(Components.interfaces.nsIAuthPrompt2)) { 1.184 + var ww = 1.185 + Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 1.186 + .getService(Components.interfaces.nsIPromptFactory); 1.187 + return ww.getPrompt(this.window, aIID); 1.188 + } 1.189 + 1.190 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.191 + } 1.192 +} 1.193 + 1.194 +const kSaveAsType_Complete = 0; // Save document with attached objects. 1.195 +// const kSaveAsType_URL = 1; // Save document or URL by itself. 1.196 +const kSaveAsType_Text = 2; // Save document, converting to plain text. 1.197 + 1.198 +/** 1.199 + * internalSave: Used when saving a document or URL. 1.200 + * 1.201 + * If aChosenData is null, this method: 1.202 + * - Determines a local target filename to use 1.203 + * - Prompts the user to confirm the destination filename and save mode 1.204 + * (aContentType affects this) 1.205 + * - [Note] This process involves the parameters aURL, aReferrer (to determine 1.206 + * how aURL was encoded), aDocument, aDefaultFileName, aFilePickerTitleKey, 1.207 + * and aSkipPrompt. 1.208 + * 1.209 + * If aChosenData is non-null, this method: 1.210 + * - Uses the provided source URI and save file name 1.211 + * - Saves the document as complete DOM if possible (aDocument present and 1.212 + * right aContentType) 1.213 + * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and 1.214 + * aSkipPrompt are ignored. 1.215 + * 1.216 + * In any case, this method: 1.217 + * - Creates a 'Persist' object (which will perform the saving in the 1.218 + * background) and then starts it. 1.219 + * - [Note] This part of the process only involves the parameters aDocument, 1.220 + * aShouldBypassCache and aReferrer. The source, the save name and the save 1.221 + * mode are the ones determined previously. 1.222 + * 1.223 + * @param aURL 1.224 + * The String representation of the URL of the document being saved 1.225 + * @param aDocument 1.226 + * The document to be saved 1.227 + * @param aDefaultFileName 1.228 + * The caller-provided suggested filename if we don't 1.229 + * find a better one 1.230 + * @param aContentDisposition 1.231 + * The caller-provided content-disposition header to use. 1.232 + * @param aContentType 1.233 + * The caller-provided content-type to use 1.234 + * @param aShouldBypassCache 1.235 + * If true, the document will always be refetched from the server 1.236 + * @param aFilePickerTitleKey 1.237 + * Alternate title for the file picker 1.238 + * @param aChosenData 1.239 + * If non-null this contains an instance of object AutoChosen (see below) 1.240 + * which holds pre-determined data so that the user does not need to be 1.241 + * prompted for a target filename. 1.242 + * @param aReferrer 1.243 + * the referrer URI object (not URL string) to use, or null 1.244 + * if no referrer should be sent. 1.245 + * @param aInitiatingDocument 1.246 + * The document from which the save was initiated. 1.247 + * @param aSkipPrompt [optional] 1.248 + * If set to true, we will attempt to save the file to the 1.249 + * default downloads folder without prompting. 1.250 + * @param aCacheKey [optional] 1.251 + * If set will be passed to saveURI. See nsIWebBrowserPersist for 1.252 + * allowed values. 1.253 + */ 1.254 +function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, 1.255 + aContentType, aShouldBypassCache, aFilePickerTitleKey, 1.256 + aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt, 1.257 + aCacheKey) 1.258 +{ 1.259 + if (aSkipPrompt == undefined) 1.260 + aSkipPrompt = false; 1.261 + 1.262 + if (aCacheKey == undefined) 1.263 + aCacheKey = null; 1.264 + 1.265 + // Note: aDocument == null when this code is used by save-link-as... 1.266 + var saveMode = GetSaveModeForContentType(aContentType, aDocument); 1.267 + 1.268 + var file, sourceURI, saveAsType; 1.269 + // Find the URI object for aURL and the FileName/Extension to use when saving. 1.270 + // FileName/Extension will be ignored if aChosenData supplied. 1.271 + if (aChosenData) { 1.272 + file = aChosenData.file; 1.273 + sourceURI = aChosenData.uri; 1.274 + saveAsType = kSaveAsType_Complete; 1.275 + 1.276 + continueSave(); 1.277 + } else { 1.278 + var charset = null; 1.279 + if (aDocument) 1.280 + charset = aDocument.characterSet; 1.281 + else if (aReferrer) 1.282 + charset = aReferrer.originCharset; 1.283 + var fileInfo = new FileInfo(aDefaultFileName); 1.284 + initFileInfo(fileInfo, aURL, charset, aDocument, 1.285 + aContentType, aContentDisposition); 1.286 + sourceURI = fileInfo.uri; 1.287 + 1.288 + var fpParams = { 1.289 + fpTitleKey: aFilePickerTitleKey, 1.290 + fileInfo: fileInfo, 1.291 + contentType: aContentType, 1.292 + saveMode: saveMode, 1.293 + saveAsType: kSaveAsType_Complete, 1.294 + file: file 1.295 + }; 1.296 + 1.297 + // Find a URI to use for determining last-downloaded-to directory 1.298 + let relatedURI = aReferrer || sourceURI; 1.299 + 1.300 + promiseTargetFile(fpParams, aSkipPrompt, relatedURI).then(aDialogAccepted => { 1.301 + if (!aDialogAccepted) 1.302 + return; 1.303 + 1.304 + saveAsType = fpParams.saveAsType; 1.305 + file = fpParams.file; 1.306 + 1.307 + continueSave(); 1.308 + }).then(null, Components.utils.reportError); 1.309 + } 1.310 + 1.311 + function continueSave() { 1.312 + // XXX We depend on the following holding true in appendFiltersForContentType(): 1.313 + // If we should save as a complete page, the saveAsType is kSaveAsType_Complete. 1.314 + // If we should save as text, the saveAsType is kSaveAsType_Text. 1.315 + var useSaveDocument = aDocument && 1.316 + (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) || 1.317 + ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text))); 1.318 + // If we're saving a document, and are saving either in complete mode or 1.319 + // as converted text, pass the document to the web browser persist component. 1.320 + // If we're just saving the HTML (second option in the list), send only the URI. 1.321 + var persistArgs = { 1.322 + sourceURI : sourceURI, 1.323 + sourceReferrer : aReferrer, 1.324 + sourceDocument : useSaveDocument ? aDocument : null, 1.325 + targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null, 1.326 + targetFile : file, 1.327 + sourceCacheKey : aCacheKey, 1.328 + sourcePostData : aDocument ? getPostData(aDocument) : null, 1.329 + bypassCache : aShouldBypassCache, 1.330 + initiatingWindow : aInitiatingDocument.defaultView 1.331 + }; 1.332 + 1.333 + // Start the actual save process 1.334 + internalPersist(persistArgs); 1.335 + } 1.336 +} 1.337 + 1.338 +/** 1.339 + * internalPersist: Creates a 'Persist' object (which will perform the saving 1.340 + * in the background) and then starts it. 1.341 + * 1.342 + * @param persistArgs.sourceURI 1.343 + * The nsIURI of the document being saved 1.344 + * @param persistArgs.sourceCacheKey [optional] 1.345 + * If set will be passed to saveURI 1.346 + * @param persistArgs.sourceDocument [optional] 1.347 + * The document to be saved, or null if not saving a complete document 1.348 + * @param persistArgs.sourceReferrer 1.349 + * Required and used only when persistArgs.sourceDocument is NOT present, 1.350 + * the nsIURI of the referrer to use, or null if no referrer should be 1.351 + * sent. 1.352 + * @param persistArgs.sourcePostData 1.353 + * Required and used only when persistArgs.sourceDocument is NOT present, 1.354 + * represents the POST data to be sent along with the HTTP request, and 1.355 + * must be null if no POST data should be sent. 1.356 + * @param persistArgs.targetFile 1.357 + * The nsIFile of the file to create 1.358 + * @param persistArgs.targetContentType 1.359 + * Required and used only when persistArgs.sourceDocument is present, 1.360 + * determines the final content type of the saved file, or null to use 1.361 + * the same content type as the source document. Currently only 1.362 + * "text/plain" is meaningful. 1.363 + * @param persistArgs.bypassCache 1.364 + * If true, the document will always be refetched from the server 1.365 + * @param persistArgs.initiatingWindow 1.366 + * The window from which the save operation was initiated. 1.367 + */ 1.368 +function internalPersist(persistArgs) 1.369 +{ 1.370 + var persist = makeWebBrowserPersist(); 1.371 + 1.372 + // Calculate persist flags. 1.373 + const nsIWBP = Components.interfaces.nsIWebBrowserPersist; 1.374 + const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES | 1.375 + nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES; 1.376 + if (persistArgs.bypassCache) 1.377 + persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE; 1.378 + else 1.379 + persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE; 1.380 + 1.381 + // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof): 1.382 + persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; 1.383 + 1.384 + // Find the URI associated with the target file 1.385 + var targetFileURL = makeFileURI(persistArgs.targetFile); 1.386 + 1.387 + var isPrivate = PrivateBrowsingUtils.isWindowPrivate(persistArgs.initiatingWindow); 1.388 + 1.389 + // Create download and initiate it (below) 1.390 + var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer); 1.391 + tr.init(persistArgs.sourceURI, 1.392 + targetFileURL, "", null, null, null, persist, isPrivate); 1.393 + persist.progressListener = new DownloadListener(window, tr); 1.394 + 1.395 + if (persistArgs.sourceDocument) { 1.396 + // Saving a Document, not a URI: 1.397 + var filesFolder = null; 1.398 + if (persistArgs.targetContentType != "text/plain") { 1.399 + // Create the local directory into which to save associated files. 1.400 + filesFolder = persistArgs.targetFile.clone(); 1.401 + 1.402 + var nameWithoutExtension = getFileBaseName(filesFolder.leafName); 1.403 + var filesFolderLeafName = 1.404 + ContentAreaUtils.stringBundle 1.405 + .formatStringFromName("filesFolder", [nameWithoutExtension], 1); 1.406 + 1.407 + filesFolder.leafName = filesFolderLeafName; 1.408 + } 1.409 + 1.410 + var encodingFlags = 0; 1.411 + if (persistArgs.targetContentType == "text/plain") { 1.412 + encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED; 1.413 + encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; 1.414 + encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; 1.415 + } 1.416 + else { 1.417 + encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; 1.418 + } 1.419 + 1.420 + const kWrapColumn = 80; 1.421 + persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder, 1.422 + persistArgs.targetContentType, encodingFlags, kWrapColumn); 1.423 + } else { 1.424 + let privacyContext = persistArgs.initiatingWindow 1.425 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 1.426 + .getInterface(Components.interfaces.nsIWebNavigation) 1.427 + .QueryInterface(Components.interfaces.nsILoadContext); 1.428 + persist.saveURI(persistArgs.sourceURI, 1.429 + persistArgs.sourceCacheKey, persistArgs.sourceReferrer, persistArgs.sourcePostData, null, 1.430 + targetFileURL, privacyContext); 1.431 + } 1.432 +} 1.433 + 1.434 +/** 1.435 + * Structure for holding info about automatically supplied parameters for 1.436 + * internalSave(...). This allows parameters to be supplied so the user does not 1.437 + * need to be prompted for file info. 1.438 + * @param aFileAutoChosen This is an nsIFile object that has been 1.439 + * pre-determined as the filename for the target to save to 1.440 + * @param aUriAutoChosen This is the nsIURI object for the target 1.441 + */ 1.442 +function AutoChosen(aFileAutoChosen, aUriAutoChosen) { 1.443 + this.file = aFileAutoChosen; 1.444 + this.uri = aUriAutoChosen; 1.445 +} 1.446 + 1.447 +/** 1.448 + * Structure for holding info about a URL and the target filename it should be 1.449 + * saved to. This structure is populated by initFileInfo(...). 1.450 + * @param aSuggestedFileName This is used by initFileInfo(...) when it 1.451 + * cannot 'discover' the filename from the url 1.452 + * @param aFileName The target filename 1.453 + * @param aFileBaseName The filename without the file extension 1.454 + * @param aFileExt The extension of the filename 1.455 + * @param aUri An nsIURI object for the url that is being saved 1.456 + */ 1.457 +function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) { 1.458 + this.suggestedFileName = aSuggestedFileName; 1.459 + this.fileName = aFileName; 1.460 + this.fileBaseName = aFileBaseName; 1.461 + this.fileExt = aFileExt; 1.462 + this.uri = aUri; 1.463 +} 1.464 + 1.465 +/** 1.466 + * Determine what the 'default' filename string is, its file extension and the 1.467 + * filename without the extension. This filename is used when prompting the user 1.468 + * for confirmation in the file picker dialog. 1.469 + * @param aFI A FileInfo structure into which we'll put the results of this method. 1.470 + * @param aURL The String representation of the URL of the document being saved 1.471 + * @param aURLCharset The charset of aURL. 1.472 + * @param aDocument The document to be saved 1.473 + * @param aContentType The content type we're saving, if it could be 1.474 + * determined by the caller. 1.475 + * @param aContentDisposition The content-disposition header for the object 1.476 + * we're saving, if it could be determined by the caller. 1.477 + */ 1.478 +function initFileInfo(aFI, aURL, aURLCharset, aDocument, 1.479 + aContentType, aContentDisposition) 1.480 +{ 1.481 + try { 1.482 + // Get an nsIURI object from aURL if possible: 1.483 + try { 1.484 + aFI.uri = makeURI(aURL, aURLCharset); 1.485 + // Assuming nsiUri is valid, calling QueryInterface(...) on it will 1.486 + // populate extra object fields (eg filename and file extension). 1.487 + var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL); 1.488 + aFI.fileExt = url.fileExtension; 1.489 + } catch (e) { 1.490 + } 1.491 + 1.492 + // Get the default filename: 1.493 + aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName), 1.494 + aFI.uri, aDocument, aContentDisposition); 1.495 + // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied 1.496 + // if saveURL(...) was the original caller (hence both aContentType and 1.497 + // aDocument are blank). If they were saving a link to a website then make 1.498 + // the extension .htm . 1.499 + if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) { 1.500 + aFI.fileExt = "htm"; 1.501 + aFI.fileBaseName = aFI.fileName; 1.502 + } else { 1.503 + aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType); 1.504 + aFI.fileBaseName = getFileBaseName(aFI.fileName); 1.505 + } 1.506 + } catch (e) { 1.507 + } 1.508 +} 1.509 + 1.510 +/** 1.511 + * Given the Filepicker Parameters (aFpP), show the file picker dialog, 1.512 + * prompting the user to confirm (or change) the fileName. 1.513 + * @param aFpP 1.514 + * A structure (see definition in internalSave(...) method) 1.515 + * containing all the data used within this method. 1.516 + * @param aSkipPrompt 1.517 + * If true, attempt to save the file automatically to the user's default 1.518 + * download directory, thus skipping the explicit prompt for a file name, 1.519 + * but only if the associated preference is set. 1.520 + * If false, don't save the file automatically to the user's 1.521 + * default download directory, even if the associated preference 1.522 + * is set, but ask for the target explicitly. 1.523 + * @param aRelatedURI 1.524 + * An nsIURI associated with the download. The last used 1.525 + * directory of the picker is retrieved from/stored in the 1.526 + * Content Pref Service using this URI. 1.527 + * @return Promise 1.528 + * @resolve a boolean. When true, it indicates that the file picker dialog 1.529 + * is accepted. 1.530 + */ 1.531 +function promiseTargetFile(aFpP, /* optional */ aSkipPrompt, /* optional */ aRelatedURI) 1.532 +{ 1.533 + return Task.spawn(function() { 1.534 + let downloadLastDir = new DownloadLastDir(window); 1.535 + let prefBranch = Services.prefs.getBranch("browser.download."); 1.536 + let useDownloadDir = prefBranch.getBoolPref("useDownloadDir"); 1.537 + 1.538 + if (!aSkipPrompt) 1.539 + useDownloadDir = false; 1.540 + 1.541 + // Default to the user's default downloads directory configured 1.542 + // through download prefs. 1.543 + let dirPath = yield Downloads.getPreferredDownloadsDirectory(); 1.544 + let dirExists = yield OS.File.exists(dirPath); 1.545 + let dir = new FileUtils.File(dirPath); 1.546 + 1.547 + if (useDownloadDir && dirExists) { 1.548 + dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName, 1.549 + aFpP.fileInfo.fileExt)); 1.550 + aFpP.file = uniqueFile(dir); 1.551 + throw new Task.Result(true); 1.552 + } 1.553 + 1.554 + // We must prompt for the file name explicitly. 1.555 + // If we must prompt because we were asked to... 1.556 + let deferred = Promise.defer(); 1.557 + if (useDownloadDir) { 1.558 + // Keep async behavior in both branches 1.559 + Services.tm.mainThread.dispatch(function() { 1.560 + deferred.resolve(null); 1.561 + }, Components.interfaces.nsIThread.DISPATCH_NORMAL); 1.562 + } else { 1.563 + downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(aFile) { 1.564 + deferred.resolve(aFile); 1.565 + }); 1.566 + } 1.567 + let file = yield deferred.promise; 1.568 + if (file && (yield OS.File.exists(file.path))) { 1.569 + dir = file; 1.570 + dirExists = true; 1.571 + } 1.572 + 1.573 + if (!dirExists) { 1.574 + // Default to desktop. 1.575 + dir = Services.dirsvc.get("Desk", Components.interfaces.nsIFile); 1.576 + } 1.577 + 1.578 + let fp = makeFilePicker(); 1.579 + let titleKey = aFpP.fpTitleKey || "SaveLinkTitle"; 1.580 + fp.init(window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey), 1.581 + Components.interfaces.nsIFilePicker.modeSave); 1.582 + 1.583 + fp.displayDirectory = dir; 1.584 + fp.defaultExtension = aFpP.fileInfo.fileExt; 1.585 + fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName, 1.586 + aFpP.fileInfo.fileExt); 1.587 + appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt, 1.588 + aFpP.saveMode); 1.589 + 1.590 + // The index of the selected filter is only preserved and restored if there's 1.591 + // more than one filter in addition to "All Files". 1.592 + if (aFpP.saveMode != SAVEMODE_FILEONLY) { 1.593 + try { 1.594 + fp.filterIndex = prefBranch.getIntPref("save_converter_index"); 1.595 + } 1.596 + catch (e) { 1.597 + } 1.598 + } 1.599 + 1.600 + let deferComplete = Promise.defer(); 1.601 + fp.open(function(aResult) { 1.602 + deferComplete.resolve(aResult); 1.603 + }); 1.604 + let result = yield deferComplete.promise; 1.605 + if (result == Components.interfaces.nsIFilePicker.returnCancel || !fp.file) { 1.606 + throw new Task.Result(false); 1.607 + } 1.608 + 1.609 + if (aFpP.saveMode != SAVEMODE_FILEONLY) 1.610 + prefBranch.setIntPref("save_converter_index", fp.filterIndex); 1.611 + 1.612 + // Do not store the last save directory as a pref inside the private browsing mode 1.613 + downloadLastDir.setFile(aRelatedURI, fp.file.parent); 1.614 + 1.615 + fp.file.leafName = validateFileName(fp.file.leafName); 1.616 + 1.617 + aFpP.saveAsType = fp.filterIndex; 1.618 + aFpP.file = fp.file; 1.619 + aFpP.fileURL = fp.fileURL; 1.620 + 1.621 + throw new Task.Result(true); 1.622 + }); 1.623 +} 1.624 + 1.625 +// Since we're automatically downloading, we don't get the file picker's 1.626 +// logic to check for existing files, so we need to do that here. 1.627 +// 1.628 +// Note - this code is identical to that in 1.629 +// mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in 1.630 +// If you are updating this code, update that code too! We can't share code 1.631 +// here since that code is called in a js component. 1.632 +function uniqueFile(aLocalFile) 1.633 +{ 1.634 + var collisionCount = 0; 1.635 + while (aLocalFile.exists()) { 1.636 + collisionCount++; 1.637 + if (collisionCount == 1) { 1.638 + // Append "(2)" before the last dot in (or at the end of) the filename 1.639 + // special case .ext.gz etc files so we don't wind up with .tar(2).gz 1.640 + if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) 1.641 + aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); 1.642 + else 1.643 + aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); 1.644 + } 1.645 + else { 1.646 + // replace the last (n) in the filename with (n+1) 1.647 + aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")"); 1.648 + } 1.649 + } 1.650 + return aLocalFile; 1.651 +} 1.652 + 1.653 +#ifdef MOZ_JSDOWNLOADS 1.654 +/** 1.655 + * Download a URL using the new jsdownloads API. 1.656 + * 1.657 + * @param aURL 1.658 + * the url to download 1.659 + * @param [optional] aFileName 1.660 + * the destination file name, if omitted will be obtained from the url. 1.661 + * @param aInitiatingDocument 1.662 + * The document from which the download was initiated. 1.663 + */ 1.664 +function DownloadURL(aURL, aFileName, aInitiatingDocument) { 1.665 + // For private browsing, try to get document out of the most recent browser 1.666 + // window, or provide our own if there's no browser window. 1.667 + let isPrivate = aInitiatingDocument.defaultView 1.668 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.669 + .getInterface(Ci.nsIWebNavigation) 1.670 + .QueryInterface(Ci.nsILoadContext) 1.671 + .usePrivateBrowsing; 1.672 + 1.673 + let fileInfo = new FileInfo(aFileName); 1.674 + initFileInfo(fileInfo, aURL, null, null, null, null); 1.675 + 1.676 + let filepickerParams = { 1.677 + fileInfo: fileInfo, 1.678 + saveMode: SAVEMODE_FILEONLY 1.679 + }; 1.680 + 1.681 + Task.spawn(function* () { 1.682 + let accepted = yield promiseTargetFile(filepickerParams, true, fileInfo.uri); 1.683 + if (!accepted) 1.684 + return; 1.685 + 1.686 + let file = filepickerParams.file; 1.687 + let download = yield Downloads.createDownload({ 1.688 + source: { url: aURL, isPrivate: isPrivate }, 1.689 + target: { path: file.path, partFilePath: file.path + ".part" } 1.690 + }); 1.691 + download.tryToKeepPartialData = true; 1.692 + download.start(); 1.693 + 1.694 + // Add the download to the list, allowing it to be managed. 1.695 + let list = yield Downloads.getList(Downloads.ALL); 1.696 + list.add(download); 1.697 + }).then(null, Components.utils.reportError); 1.698 +} 1.699 +#endif 1.700 + 1.701 +// We have no DOM, and can only save the URL as is. 1.702 +const SAVEMODE_FILEONLY = 0x00; 1.703 +// We have a DOM and can save as complete. 1.704 +const SAVEMODE_COMPLETE_DOM = 0x01; 1.705 +// We have a DOM which we can serialize as text. 1.706 +const SAVEMODE_COMPLETE_TEXT = 0x02; 1.707 + 1.708 +// If we are able to save a complete DOM, the 'save as complete' filter 1.709 +// must be the first filter appended. The 'save page only' counterpart 1.710 +// must be the second filter appended. And the 'save as complete text' 1.711 +// filter must be the third filter appended. 1.712 +function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode) 1.713 +{ 1.714 + // The bundle name for saving only a specific content type. 1.715 + var bundleName; 1.716 + // The corresponding filter string for a specific content type. 1.717 + var filterString; 1.718 + 1.719 + // XXX all the cases that are handled explicitly here MUST be handled 1.720 + // in GetSaveModeForContentType to return a non-fileonly filter. 1.721 + switch (aContentType) { 1.722 + case "text/html": 1.723 + bundleName = "WebPageHTMLOnlyFilter"; 1.724 + filterString = "*.htm; *.html"; 1.725 + break; 1.726 + 1.727 + case "application/xhtml+xml": 1.728 + bundleName = "WebPageXHTMLOnlyFilter"; 1.729 + filterString = "*.xht; *.xhtml"; 1.730 + break; 1.731 + 1.732 + case "image/svg+xml": 1.733 + bundleName = "WebPageSVGOnlyFilter"; 1.734 + filterString = "*.svg; *.svgz"; 1.735 + break; 1.736 + 1.737 + case "text/xml": 1.738 + case "application/xml": 1.739 + bundleName = "WebPageXMLOnlyFilter"; 1.740 + filterString = "*.xml"; 1.741 + break; 1.742 + 1.743 + default: 1.744 + if (aSaveMode != SAVEMODE_FILEONLY) 1.745 + throw "Invalid save mode for type '" + aContentType + "'"; 1.746 + 1.747 + var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension); 1.748 + if (mimeInfo) { 1.749 + 1.750 + var extEnumerator = mimeInfo.getFileExtensions(); 1.751 + 1.752 + var extString = ""; 1.753 + while (extEnumerator.hasMore()) { 1.754 + var extension = extEnumerator.getNext(); 1.755 + if (extString) 1.756 + extString += "; "; // If adding more than one extension, 1.757 + // separate by semi-colon 1.758 + extString += "*." + extension; 1.759 + } 1.760 + 1.761 + if (extString) 1.762 + aFilePicker.appendFilter(mimeInfo.description, extString); 1.763 + } 1.764 + 1.765 + break; 1.766 + } 1.767 + 1.768 + if (aSaveMode & SAVEMODE_COMPLETE_DOM) { 1.769 + aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"), 1.770 + filterString); 1.771 + // We should always offer a choice to save document only if 1.772 + // we allow saving as complete. 1.773 + aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName(bundleName), 1.774 + filterString); 1.775 + } 1.776 + 1.777 + if (aSaveMode & SAVEMODE_COMPLETE_TEXT) 1.778 + aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText); 1.779 + 1.780 + // Always append the all files (*) filter 1.781 + aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll); 1.782 +} 1.783 + 1.784 +function getPostData(aDocument) 1.785 +{ 1.786 + try { 1.787 + // Find the session history entry corresponding to the given document. In 1.788 + // the current implementation, nsIWebPageDescriptor.currentDescriptor always 1.789 + // returns a session history entry. 1.790 + var sessionHistoryEntry = 1.791 + aDocument.defaultView 1.792 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 1.793 + .getInterface(Components.interfaces.nsIWebNavigation) 1.794 + .QueryInterface(Components.interfaces.nsIWebPageDescriptor) 1.795 + .currentDescriptor 1.796 + .QueryInterface(Components.interfaces.nsISHEntry); 1.797 + return sessionHistoryEntry.postData; 1.798 + } 1.799 + catch (e) { 1.800 + } 1.801 + return null; 1.802 +} 1.803 + 1.804 +function makeWebBrowserPersist() 1.805 +{ 1.806 + const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"; 1.807 + const persistIID = Components.interfaces.nsIWebBrowserPersist; 1.808 + return Components.classes[persistContractID].createInstance(persistIID); 1.809 +} 1.810 + 1.811 +function makeURI(aURL, aOriginCharset, aBaseURI) 1.812 +{ 1.813 + return BrowserUtils.makeURI(aURL, aOriginCharset, aBaseURI); 1.814 +} 1.815 + 1.816 +function makeFileURI(aFile) 1.817 +{ 1.818 + return BrowserUtils.makeFileURI(aFile); 1.819 +} 1.820 + 1.821 +function makeFilePicker() 1.822 +{ 1.823 + const fpContractID = "@mozilla.org/filepicker;1"; 1.824 + const fpIID = Components.interfaces.nsIFilePicker; 1.825 + return Components.classes[fpContractID].createInstance(fpIID); 1.826 +} 1.827 + 1.828 +function getMIMEService() 1.829 +{ 1.830 + const mimeSvcContractID = "@mozilla.org/mime;1"; 1.831 + const mimeSvcIID = Components.interfaces.nsIMIMEService; 1.832 + const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID); 1.833 + return mimeSvc; 1.834 +} 1.835 + 1.836 +// Given aFileName, find the fileName without the extension on the end. 1.837 +function getFileBaseName(aFileName) 1.838 +{ 1.839 + // Remove the file extension from aFileName: 1.840 + return aFileName.replace(/\.[^.]*$/, ""); 1.841 +} 1.842 + 1.843 +function getMIMETypeForURI(aURI) 1.844 +{ 1.845 + try { 1.846 + return getMIMEService().getTypeFromURI(aURI); 1.847 + } 1.848 + catch (e) { 1.849 + } 1.850 + return null; 1.851 +} 1.852 + 1.853 +function getMIMEInfoForType(aMIMEType, aExtension) 1.854 +{ 1.855 + if (aMIMEType || aExtension) { 1.856 + try { 1.857 + return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension); 1.858 + } 1.859 + catch (e) { 1.860 + } 1.861 + } 1.862 + return null; 1.863 +} 1.864 + 1.865 +function getDefaultFileName(aDefaultFileName, aURI, aDocument, 1.866 + aContentDisposition) 1.867 +{ 1.868 + // 1) look for a filename in the content-disposition header, if any 1.869 + if (aContentDisposition) { 1.870 + const mhpContractID = "@mozilla.org/network/mime-hdrparam;1"; 1.871 + const mhpIID = Components.interfaces.nsIMIMEHeaderParam; 1.872 + const mhp = Components.classes[mhpContractID].getService(mhpIID); 1.873 + var dummy = { value: null }; // Need an out param... 1.874 + var charset = getCharsetforSave(aDocument); 1.875 + 1.876 + var fileName = null; 1.877 + try { 1.878 + fileName = mhp.getParameter(aContentDisposition, "filename", charset, 1.879 + true, dummy); 1.880 + } 1.881 + catch (e) { 1.882 + try { 1.883 + fileName = mhp.getParameter(aContentDisposition, "name", charset, true, 1.884 + dummy); 1.885 + } 1.886 + catch (e) { 1.887 + } 1.888 + } 1.889 + if (fileName) 1.890 + return fileName; 1.891 + } 1.892 + 1.893 + let docTitle; 1.894 + if (aDocument) { 1.895 + // If the document looks like HTML or XML, try to use its original title. 1.896 + docTitle = validateFileName(aDocument.title).trim(); 1.897 + if (docTitle) { 1.898 + let contentType = aDocument.contentType; 1.899 + if (contentType == "application/xhtml+xml" || 1.900 + contentType == "application/xml" || 1.901 + contentType == "image/svg+xml" || 1.902 + contentType == "text/html" || 1.903 + contentType == "text/xml") { 1.904 + // 2) Use the document title 1.905 + return docTitle; 1.906 + } 1.907 + } 1.908 + } 1.909 + 1.910 + try { 1.911 + var url = aURI.QueryInterface(Components.interfaces.nsIURL); 1.912 + if (url.fileName != "") { 1.913 + // 3) Use the actual file name, if present 1.914 + var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"] 1.915 + .getService(Components.interfaces.nsITextToSubURI); 1.916 + return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName)); 1.917 + } 1.918 + } catch (e) { 1.919 + // This is something like a data: and so forth URI... no filename here. 1.920 + } 1.921 + 1.922 + if (docTitle) 1.923 + // 4) Use the document title 1.924 + return docTitle; 1.925 + 1.926 + if (aDefaultFileName) 1.927 + // 5) Use the caller-provided name, if any 1.928 + return validateFileName(aDefaultFileName); 1.929 + 1.930 + // 6) If this is a directory, use the last directory name 1.931 + var path = aURI.path.match(/\/([^\/]+)\/$/); 1.932 + if (path && path.length > 1) 1.933 + return validateFileName(path[1]); 1.934 + 1.935 + try { 1.936 + if (aURI.host) 1.937 + // 7) Use the host. 1.938 + return aURI.host; 1.939 + } catch (e) { 1.940 + // Some files have no information at all, like Javascript generated pages 1.941 + } 1.942 + try { 1.943 + // 8) Use the default file name 1.944 + return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName"); 1.945 + } catch (e) { 1.946 + //in case localized string cannot be found 1.947 + } 1.948 + // 9) If all else fails, use "index" 1.949 + return "index"; 1.950 +} 1.951 + 1.952 +function validateFileName(aFileName) 1.953 +{ 1.954 + var re = /[\/]+/g; 1.955 + if (navigator.appVersion.indexOf("Windows") != -1) { 1.956 + re = /[\\\/\|]+/g; 1.957 + aFileName = aFileName.replace(/[\"]+/g, "'"); 1.958 + aFileName = aFileName.replace(/[\*\:\?]+/g, " "); 1.959 + aFileName = aFileName.replace(/[\<]+/g, "("); 1.960 + aFileName = aFileName.replace(/[\>]+/g, ")"); 1.961 + } 1.962 + else if (navigator.appVersion.indexOf("Macintosh") != -1) 1.963 + re = /[\:\/]+/g; 1.964 + else if (navigator.appVersion.indexOf("Android") != -1) { 1.965 + // On mobile devices, the filesystem may be very limited in what 1.966 + // it considers valid characters. To avoid errors, we sanitize 1.967 + // conservatively. 1.968 + const dangerousChars = "*?<>|\":/\\[];,+="; 1.969 + var processed = ""; 1.970 + for (var i = 0; i < aFileName.length; i++) 1.971 + processed += aFileName.charCodeAt(i) >= 32 && 1.972 + !(dangerousChars.indexOf(aFileName[i]) >= 0) ? aFileName[i] 1.973 + : "_"; 1.974 + 1.975 + // Last character should not be a space 1.976 + processed = processed.trim(); 1.977 + 1.978 + // If a large part of the filename has been sanitized, then we 1.979 + // will use a default filename instead 1.980 + if (processed.replace(/_/g, "").length <= processed.length/2) { 1.981 + // We purposefully do not use a localized default filename, 1.982 + // which we could have done using 1.983 + // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName") 1.984 + // since it may contain invalid characters. 1.985 + var original = processed; 1.986 + processed = "download"; 1.987 + 1.988 + // Preserve a suffix, if there is one 1.989 + if (original.indexOf(".") >= 0) { 1.990 + var suffix = original.split(".").slice(-1)[0]; 1.991 + if (suffix && suffix.indexOf("_") < 0) 1.992 + processed += "." + suffix; 1.993 + } 1.994 + } 1.995 + return processed; 1.996 + } 1.997 + 1.998 + return aFileName.replace(re, "_"); 1.999 +} 1.1000 + 1.1001 +function getNormalizedLeafName(aFile, aDefaultExtension) 1.1002 +{ 1.1003 + if (!aDefaultExtension) 1.1004 + return aFile; 1.1005 + 1.1006 +#ifdef XP_WIN 1.1007 + // Remove trailing dots and spaces on windows 1.1008 + aFile = aFile.replace(/[\s.]+$/, ""); 1.1009 +#endif 1.1010 + 1.1011 + // Remove leading dots 1.1012 + aFile = aFile.replace(/^\.+/, ""); 1.1013 + 1.1014 + // Fix up the file name we're saving to to include the default extension 1.1015 + var i = aFile.lastIndexOf("."); 1.1016 + if (aFile.substr(i + 1) != aDefaultExtension) 1.1017 + return aFile + "." + aDefaultExtension; 1.1018 + 1.1019 + return aFile; 1.1020 +} 1.1021 + 1.1022 +function getDefaultExtension(aFilename, aURI, aContentType) 1.1023 +{ 1.1024 + if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp") 1.1025 + return ""; // temporary fix for bug 120327 1.1026 + 1.1027 + // First try the extension from the filename 1.1028 + const stdURLContractID = "@mozilla.org/network/standard-url;1"; 1.1029 + const stdURLIID = Components.interfaces.nsIURL; 1.1030 + var url = Components.classes[stdURLContractID].createInstance(stdURLIID); 1.1031 + url.filePath = aFilename; 1.1032 + 1.1033 + var ext = url.fileExtension; 1.1034 + 1.1035 + // This mirrors some code in nsExternalHelperAppService::DoContent 1.1036 + // Use the filename first and then the URI if that fails 1.1037 + 1.1038 + var mimeInfo = getMIMEInfoForType(aContentType, ext); 1.1039 + 1.1040 + if (ext && mimeInfo && mimeInfo.extensionExists(ext)) 1.1041 + return ext; 1.1042 + 1.1043 + // Well, that failed. Now try the extension from the URI 1.1044 + var urlext; 1.1045 + try { 1.1046 + url = aURI.QueryInterface(Components.interfaces.nsIURL); 1.1047 + urlext = url.fileExtension; 1.1048 + } catch (e) { 1.1049 + } 1.1050 + 1.1051 + if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) { 1.1052 + return urlext; 1.1053 + } 1.1054 + else { 1.1055 + try { 1.1056 + if (mimeInfo) 1.1057 + return mimeInfo.primaryExtension; 1.1058 + } 1.1059 + catch (e) { 1.1060 + } 1.1061 + // Fall back on the extensions in the filename and URI for lack 1.1062 + // of anything better. 1.1063 + return ext || urlext; 1.1064 + } 1.1065 +} 1.1066 + 1.1067 +function GetSaveModeForContentType(aContentType, aDocument) 1.1068 +{ 1.1069 + // We can only save a complete page if we have a loaded document 1.1070 + if (!aDocument) 1.1071 + return SAVEMODE_FILEONLY; 1.1072 + 1.1073 + // Find the possible save modes using the provided content type 1.1074 + var saveMode = SAVEMODE_FILEONLY; 1.1075 + switch (aContentType) { 1.1076 + case "text/html": 1.1077 + case "application/xhtml+xml": 1.1078 + case "image/svg+xml": 1.1079 + saveMode |= SAVEMODE_COMPLETE_TEXT; 1.1080 + // Fall through 1.1081 + case "text/xml": 1.1082 + case "application/xml": 1.1083 + saveMode |= SAVEMODE_COMPLETE_DOM; 1.1084 + break; 1.1085 + } 1.1086 + 1.1087 + return saveMode; 1.1088 +} 1.1089 + 1.1090 +function getCharsetforSave(aDocument) 1.1091 +{ 1.1092 + if (aDocument) 1.1093 + return aDocument.characterSet; 1.1094 + 1.1095 + if (document.commandDispatcher.focusedWindow) 1.1096 + return document.commandDispatcher.focusedWindow.document.characterSet; 1.1097 + 1.1098 + return window.content.document.characterSet; 1.1099 +} 1.1100 + 1.1101 +/** 1.1102 + * Open a URL from chrome, determining if we can handle it internally or need to 1.1103 + * launch an external application to handle it. 1.1104 + * @param aURL The URL to be opened 1.1105 + */ 1.1106 +function openURL(aURL) 1.1107 +{ 1.1108 + var uri = makeURI(aURL); 1.1109 + 1.1110 + var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"] 1.1111 + .getService(Components.interfaces.nsIExternalProtocolService); 1.1112 + 1.1113 + if (!protocolSvc.isExposedProtocol(uri.scheme)) { 1.1114 + // If we're not a browser, use the external protocol service to load the URI. 1.1115 + protocolSvc.loadUrl(uri); 1.1116 + } 1.1117 + else { 1.1118 + var recentWindow = Services.wm.getMostRecentWindow("navigator:browser"); 1.1119 + if (recentWindow) { 1.1120 + recentWindow.openUILinkIn(uri.spec, "tab"); 1.1121 + return; 1.1122 + } 1.1123 + 1.1124 + var loadgroup = Components.classes["@mozilla.org/network/load-group;1"] 1.1125 + .createInstance(Components.interfaces.nsILoadGroup); 1.1126 + var appstartup = Services.startup; 1.1127 + 1.1128 + var loadListener = { 1.1129 + onStartRequest: function ll_start(aRequest, aContext) { 1.1130 + appstartup.enterLastWindowClosingSurvivalArea(); 1.1131 + }, 1.1132 + onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) { 1.1133 + appstartup.exitLastWindowClosingSurvivalArea(); 1.1134 + }, 1.1135 + QueryInterface: function ll_QI(iid) { 1.1136 + if (iid.equals(Components.interfaces.nsISupports) || 1.1137 + iid.equals(Components.interfaces.nsIRequestObserver) || 1.1138 + iid.equals(Components.interfaces.nsISupportsWeakReference)) 1.1139 + return this; 1.1140 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.1141 + } 1.1142 + } 1.1143 + loadgroup.groupObserver = loadListener; 1.1144 + 1.1145 + var uriListener = { 1.1146 + onStartURIOpen: function(uri) { return false; }, 1.1147 + doContent: function(ctype, preferred, request, handler) { return false; }, 1.1148 + isPreferred: function(ctype, desired) { return false; }, 1.1149 + canHandleContent: function(ctype, preferred, desired) { return false; }, 1.1150 + loadCookie: null, 1.1151 + parentContentListener: null, 1.1152 + getInterface: function(iid) { 1.1153 + if (iid.equals(Components.interfaces.nsIURIContentListener)) 1.1154 + return this; 1.1155 + if (iid.equals(Components.interfaces.nsILoadGroup)) 1.1156 + return loadgroup; 1.1157 + throw Components.results.NS_ERROR_NO_INTERFACE; 1.1158 + } 1.1159 + } 1.1160 + 1.1161 + var channel = Services.io.newChannelFromURI(uri); 1.1162 + var uriLoader = Components.classes["@mozilla.org/uriloader;1"] 1.1163 + .getService(Components.interfaces.nsIURILoader); 1.1164 + uriLoader.openURI(channel, 1.1165 + Components.interfaces.nsIURILoader.IS_CONTENT_PREFERRED, 1.1166 + uriListener); 1.1167 + } 1.1168 +}