toolkit/content/contentAreaUtils.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 # -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
michael@0 2 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 # License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 5
michael@0 6 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 7
michael@0 8 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
michael@0 9 "resource://gre/modules/BrowserUtils.jsm");
michael@0 10 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
michael@0 11 "resource://gre/modules/Downloads.jsm");
michael@0 12 XPCOMUtils.defineLazyModuleGetter(this, "DownloadLastDir",
michael@0 13 "resource://gre/modules/DownloadLastDir.jsm");
michael@0 14 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
michael@0 15 "resource://gre/modules/FileUtils.jsm");
michael@0 16 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 17 "resource://gre/modules/osfile.jsm");
michael@0 18 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
michael@0 19 "resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 20 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 21 "resource://gre/modules/Promise.jsm");
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "Services",
michael@0 23 "resource://gre/modules/Services.jsm");
michael@0 24 XPCOMUtils.defineLazyModuleGetter(this, "Task",
michael@0 25 "resource://gre/modules/Task.jsm");
michael@0 26 var ContentAreaUtils = {
michael@0 27
michael@0 28 // this is for backwards compatibility.
michael@0 29 get ioService() {
michael@0 30 return Services.io;
michael@0 31 },
michael@0 32
michael@0 33 get stringBundle() {
michael@0 34 delete this.stringBundle;
michael@0 35 return this.stringBundle =
michael@0 36 Services.strings.createBundle("chrome://global/locale/contentAreaCommands.properties");
michael@0 37 }
michael@0 38 }
michael@0 39
michael@0 40 function urlSecurityCheck(aURL, aPrincipal, aFlags)
michael@0 41 {
michael@0 42 return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags);
michael@0 43 }
michael@0 44
michael@0 45 /**
michael@0 46 * Determine whether or not a given focused DOMWindow is in the content area.
michael@0 47 **/
michael@0 48 function isContentFrame(aFocusedWindow)
michael@0 49 {
michael@0 50 if (!aFocusedWindow)
michael@0 51 return false;
michael@0 52
michael@0 53 return (aFocusedWindow.top == window.content);
michael@0 54 }
michael@0 55
michael@0 56 // Clientele: (Make sure you don't break any of these)
michael@0 57 // - File -> Save Page/Frame As...
michael@0 58 // - Context -> Save Page/Frame As...
michael@0 59 // - Context -> Save Link As...
michael@0 60 // - Alt-Click links in web pages
michael@0 61 // - Alt-Click links in the UI
michael@0 62 //
michael@0 63 // Try saving each of these types:
michael@0 64 // - A complete webpage using File->Save Page As, and Context->Save Page As
michael@0 65 // - A webpage as HTML only using the above methods
michael@0 66 // - A webpage as Text only using the above methods
michael@0 67 // - An image with an extension (e.g. .jpg) in its file name, using
michael@0 68 // Context->Save Image As...
michael@0 69 // - An image without an extension (e.g. a banner ad on cnn.com) using
michael@0 70 // the above method.
michael@0 71 // - A linked document using Save Link As...
michael@0 72 // - A linked document using Alt-click Save Link As...
michael@0 73 //
michael@0 74 function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
michael@0 75 aSkipPrompt, aReferrer, aSourceDocument)
michael@0 76 {
michael@0 77 internalSave(aURL, null, aFileName, null, null, aShouldBypassCache,
michael@0 78 aFilePickerTitleKey, null, aReferrer, aSourceDocument,
michael@0 79 aSkipPrompt, null);
michael@0 80 }
michael@0 81
michael@0 82 // Just like saveURL, but will get some info off the image before
michael@0 83 // calling internalSave
michael@0 84 // Clientele: (Make sure you don't break any of these)
michael@0 85 // - Context -> Save Image As...
michael@0 86 const imgICache = Components.interfaces.imgICache;
michael@0 87 const nsISupportsCString = Components.interfaces.nsISupportsCString;
michael@0 88
michael@0 89 function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
michael@0 90 aSkipPrompt, aReferrer, aDoc)
michael@0 91 {
michael@0 92 var contentType = null;
michael@0 93 var contentDisposition = null;
michael@0 94 if (!aShouldBypassCache) {
michael@0 95 try {
michael@0 96 var imageCache = Components.classes["@mozilla.org/image/tools;1"]
michael@0 97 .getService(Components.interfaces.imgITools)
michael@0 98 .getImgCacheForDocument(aDoc);
michael@0 99 var props =
michael@0 100 imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null)));
michael@0 101 if (props) {
michael@0 102 contentType = props.get("type", nsISupportsCString);
michael@0 103 contentDisposition = props.get("content-disposition",
michael@0 104 nsISupportsCString);
michael@0 105 }
michael@0 106 } catch (e) {
michael@0 107 // Failure to get type and content-disposition off the image is non-fatal
michael@0 108 }
michael@0 109 }
michael@0 110 internalSave(aURL, null, aFileName, contentDisposition, contentType,
michael@0 111 aShouldBypassCache, aFilePickerTitleKey, null, aReferrer,
michael@0 112 aDoc, aSkipPrompt, null);
michael@0 113 }
michael@0 114
michael@0 115 function saveDocument(aDocument, aSkipPrompt)
michael@0 116 {
michael@0 117 if (!aDocument)
michael@0 118 throw "Must have a document when calling saveDocument";
michael@0 119
michael@0 120 // We want to use cached data because the document is currently visible.
michael@0 121 var ifreq =
michael@0 122 aDocument.defaultView
michael@0 123 .QueryInterface(Components.interfaces.nsIInterfaceRequestor);
michael@0 124
michael@0 125 var contentDisposition = null;
michael@0 126 try {
michael@0 127 contentDisposition =
michael@0 128 ifreq.getInterface(Components.interfaces.nsIDOMWindowUtils)
michael@0 129 .getDocumentMetadata("content-disposition");
michael@0 130 } catch (ex) {
michael@0 131 // Failure to get a content-disposition is ok
michael@0 132 }
michael@0 133
michael@0 134 var cacheKey = null;
michael@0 135 try {
michael@0 136 cacheKey =
michael@0 137 ifreq.getInterface(Components.interfaces.nsIWebNavigation)
michael@0 138 .QueryInterface(Components.interfaces.nsIWebPageDescriptor);
michael@0 139 } catch (ex) {
michael@0 140 // We might not find it in the cache. Oh, well.
michael@0 141 }
michael@0 142
michael@0 143 internalSave(aDocument.location.href, aDocument, null, contentDisposition,
michael@0 144 aDocument.contentType, false, null, null,
michael@0 145 aDocument.referrer ? makeURI(aDocument.referrer) : null,
michael@0 146 aDocument, aSkipPrompt, cacheKey);
michael@0 147 }
michael@0 148
michael@0 149 function DownloadListener(win, transfer) {
michael@0 150 function makeClosure(name) {
michael@0 151 return function() {
michael@0 152 transfer[name].apply(transfer, arguments);
michael@0 153 }
michael@0 154 }
michael@0 155
michael@0 156 this.window = win;
michael@0 157
michael@0 158 // Now... we need to forward all calls to our transfer
michael@0 159 for (var i in transfer) {
michael@0 160 if (i != "QueryInterface")
michael@0 161 this[i] = makeClosure(i);
michael@0 162 }
michael@0 163 }
michael@0 164
michael@0 165 DownloadListener.prototype = {
michael@0 166 QueryInterface: function dl_qi(aIID)
michael@0 167 {
michael@0 168 if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) ||
michael@0 169 aIID.equals(Components.interfaces.nsIWebProgressListener) ||
michael@0 170 aIID.equals(Components.interfaces.nsIWebProgressListener2) ||
michael@0 171 aIID.equals(Components.interfaces.nsISupports)) {
michael@0 172 return this;
michael@0 173 }
michael@0 174 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 175 },
michael@0 176
michael@0 177 getInterface: function dl_gi(aIID)
michael@0 178 {
michael@0 179 if (aIID.equals(Components.interfaces.nsIAuthPrompt) ||
michael@0 180 aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
michael@0 181 var ww =
michael@0 182 Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
michael@0 183 .getService(Components.interfaces.nsIPromptFactory);
michael@0 184 return ww.getPrompt(this.window, aIID);
michael@0 185 }
michael@0 186
michael@0 187 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 188 }
michael@0 189 }
michael@0 190
michael@0 191 const kSaveAsType_Complete = 0; // Save document with attached objects.
michael@0 192 // const kSaveAsType_URL = 1; // Save document or URL by itself.
michael@0 193 const kSaveAsType_Text = 2; // Save document, converting to plain text.
michael@0 194
michael@0 195 /**
michael@0 196 * internalSave: Used when saving a document or URL.
michael@0 197 *
michael@0 198 * If aChosenData is null, this method:
michael@0 199 * - Determines a local target filename to use
michael@0 200 * - Prompts the user to confirm the destination filename and save mode
michael@0 201 * (aContentType affects this)
michael@0 202 * - [Note] This process involves the parameters aURL, aReferrer (to determine
michael@0 203 * how aURL was encoded), aDocument, aDefaultFileName, aFilePickerTitleKey,
michael@0 204 * and aSkipPrompt.
michael@0 205 *
michael@0 206 * If aChosenData is non-null, this method:
michael@0 207 * - Uses the provided source URI and save file name
michael@0 208 * - Saves the document as complete DOM if possible (aDocument present and
michael@0 209 * right aContentType)
michael@0 210 * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and
michael@0 211 * aSkipPrompt are ignored.
michael@0 212 *
michael@0 213 * In any case, this method:
michael@0 214 * - Creates a 'Persist' object (which will perform the saving in the
michael@0 215 * background) and then starts it.
michael@0 216 * - [Note] This part of the process only involves the parameters aDocument,
michael@0 217 * aShouldBypassCache and aReferrer. The source, the save name and the save
michael@0 218 * mode are the ones determined previously.
michael@0 219 *
michael@0 220 * @param aURL
michael@0 221 * The String representation of the URL of the document being saved
michael@0 222 * @param aDocument
michael@0 223 * The document to be saved
michael@0 224 * @param aDefaultFileName
michael@0 225 * The caller-provided suggested filename if we don't
michael@0 226 * find a better one
michael@0 227 * @param aContentDisposition
michael@0 228 * The caller-provided content-disposition header to use.
michael@0 229 * @param aContentType
michael@0 230 * The caller-provided content-type to use
michael@0 231 * @param aShouldBypassCache
michael@0 232 * If true, the document will always be refetched from the server
michael@0 233 * @param aFilePickerTitleKey
michael@0 234 * Alternate title for the file picker
michael@0 235 * @param aChosenData
michael@0 236 * If non-null this contains an instance of object AutoChosen (see below)
michael@0 237 * which holds pre-determined data so that the user does not need to be
michael@0 238 * prompted for a target filename.
michael@0 239 * @param aReferrer
michael@0 240 * the referrer URI object (not URL string) to use, or null
michael@0 241 * if no referrer should be sent.
michael@0 242 * @param aInitiatingDocument
michael@0 243 * The document from which the save was initiated.
michael@0 244 * @param aSkipPrompt [optional]
michael@0 245 * If set to true, we will attempt to save the file to the
michael@0 246 * default downloads folder without prompting.
michael@0 247 * @param aCacheKey [optional]
michael@0 248 * If set will be passed to saveURI. See nsIWebBrowserPersist for
michael@0 249 * allowed values.
michael@0 250 */
michael@0 251 function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
michael@0 252 aContentType, aShouldBypassCache, aFilePickerTitleKey,
michael@0 253 aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt,
michael@0 254 aCacheKey)
michael@0 255 {
michael@0 256 if (aSkipPrompt == undefined)
michael@0 257 aSkipPrompt = false;
michael@0 258
michael@0 259 if (aCacheKey == undefined)
michael@0 260 aCacheKey = null;
michael@0 261
michael@0 262 // Note: aDocument == null when this code is used by save-link-as...
michael@0 263 var saveMode = GetSaveModeForContentType(aContentType, aDocument);
michael@0 264
michael@0 265 var file, sourceURI, saveAsType;
michael@0 266 // Find the URI object for aURL and the FileName/Extension to use when saving.
michael@0 267 // FileName/Extension will be ignored if aChosenData supplied.
michael@0 268 if (aChosenData) {
michael@0 269 file = aChosenData.file;
michael@0 270 sourceURI = aChosenData.uri;
michael@0 271 saveAsType = kSaveAsType_Complete;
michael@0 272
michael@0 273 continueSave();
michael@0 274 } else {
michael@0 275 var charset = null;
michael@0 276 if (aDocument)
michael@0 277 charset = aDocument.characterSet;
michael@0 278 else if (aReferrer)
michael@0 279 charset = aReferrer.originCharset;
michael@0 280 var fileInfo = new FileInfo(aDefaultFileName);
michael@0 281 initFileInfo(fileInfo, aURL, charset, aDocument,
michael@0 282 aContentType, aContentDisposition);
michael@0 283 sourceURI = fileInfo.uri;
michael@0 284
michael@0 285 var fpParams = {
michael@0 286 fpTitleKey: aFilePickerTitleKey,
michael@0 287 fileInfo: fileInfo,
michael@0 288 contentType: aContentType,
michael@0 289 saveMode: saveMode,
michael@0 290 saveAsType: kSaveAsType_Complete,
michael@0 291 file: file
michael@0 292 };
michael@0 293
michael@0 294 // Find a URI to use for determining last-downloaded-to directory
michael@0 295 let relatedURI = aReferrer || sourceURI;
michael@0 296
michael@0 297 promiseTargetFile(fpParams, aSkipPrompt, relatedURI).then(aDialogAccepted => {
michael@0 298 if (!aDialogAccepted)
michael@0 299 return;
michael@0 300
michael@0 301 saveAsType = fpParams.saveAsType;
michael@0 302 file = fpParams.file;
michael@0 303
michael@0 304 continueSave();
michael@0 305 }).then(null, Components.utils.reportError);
michael@0 306 }
michael@0 307
michael@0 308 function continueSave() {
michael@0 309 // XXX We depend on the following holding true in appendFiltersForContentType():
michael@0 310 // If we should save as a complete page, the saveAsType is kSaveAsType_Complete.
michael@0 311 // If we should save as text, the saveAsType is kSaveAsType_Text.
michael@0 312 var useSaveDocument = aDocument &&
michael@0 313 (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) ||
michael@0 314 ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text)));
michael@0 315 // If we're saving a document, and are saving either in complete mode or
michael@0 316 // as converted text, pass the document to the web browser persist component.
michael@0 317 // If we're just saving the HTML (second option in the list), send only the URI.
michael@0 318 var persistArgs = {
michael@0 319 sourceURI : sourceURI,
michael@0 320 sourceReferrer : aReferrer,
michael@0 321 sourceDocument : useSaveDocument ? aDocument : null,
michael@0 322 targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null,
michael@0 323 targetFile : file,
michael@0 324 sourceCacheKey : aCacheKey,
michael@0 325 sourcePostData : aDocument ? getPostData(aDocument) : null,
michael@0 326 bypassCache : aShouldBypassCache,
michael@0 327 initiatingWindow : aInitiatingDocument.defaultView
michael@0 328 };
michael@0 329
michael@0 330 // Start the actual save process
michael@0 331 internalPersist(persistArgs);
michael@0 332 }
michael@0 333 }
michael@0 334
michael@0 335 /**
michael@0 336 * internalPersist: Creates a 'Persist' object (which will perform the saving
michael@0 337 * in the background) and then starts it.
michael@0 338 *
michael@0 339 * @param persistArgs.sourceURI
michael@0 340 * The nsIURI of the document being saved
michael@0 341 * @param persistArgs.sourceCacheKey [optional]
michael@0 342 * If set will be passed to saveURI
michael@0 343 * @param persistArgs.sourceDocument [optional]
michael@0 344 * The document to be saved, or null if not saving a complete document
michael@0 345 * @param persistArgs.sourceReferrer
michael@0 346 * Required and used only when persistArgs.sourceDocument is NOT present,
michael@0 347 * the nsIURI of the referrer to use, or null if no referrer should be
michael@0 348 * sent.
michael@0 349 * @param persistArgs.sourcePostData
michael@0 350 * Required and used only when persistArgs.sourceDocument is NOT present,
michael@0 351 * represents the POST data to be sent along with the HTTP request, and
michael@0 352 * must be null if no POST data should be sent.
michael@0 353 * @param persistArgs.targetFile
michael@0 354 * The nsIFile of the file to create
michael@0 355 * @param persistArgs.targetContentType
michael@0 356 * Required and used only when persistArgs.sourceDocument is present,
michael@0 357 * determines the final content type of the saved file, or null to use
michael@0 358 * the same content type as the source document. Currently only
michael@0 359 * "text/plain" is meaningful.
michael@0 360 * @param persistArgs.bypassCache
michael@0 361 * If true, the document will always be refetched from the server
michael@0 362 * @param persistArgs.initiatingWindow
michael@0 363 * The window from which the save operation was initiated.
michael@0 364 */
michael@0 365 function internalPersist(persistArgs)
michael@0 366 {
michael@0 367 var persist = makeWebBrowserPersist();
michael@0 368
michael@0 369 // Calculate persist flags.
michael@0 370 const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
michael@0 371 const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
michael@0 372 nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES;
michael@0 373 if (persistArgs.bypassCache)
michael@0 374 persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
michael@0 375 else
michael@0 376 persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE;
michael@0 377
michael@0 378 // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof):
michael@0 379 persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
michael@0 380
michael@0 381 // Find the URI associated with the target file
michael@0 382 var targetFileURL = makeFileURI(persistArgs.targetFile);
michael@0 383
michael@0 384 var isPrivate = PrivateBrowsingUtils.isWindowPrivate(persistArgs.initiatingWindow);
michael@0 385
michael@0 386 // Create download and initiate it (below)
michael@0 387 var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer);
michael@0 388 tr.init(persistArgs.sourceURI,
michael@0 389 targetFileURL, "", null, null, null, persist, isPrivate);
michael@0 390 persist.progressListener = new DownloadListener(window, tr);
michael@0 391
michael@0 392 if (persistArgs.sourceDocument) {
michael@0 393 // Saving a Document, not a URI:
michael@0 394 var filesFolder = null;
michael@0 395 if (persistArgs.targetContentType != "text/plain") {
michael@0 396 // Create the local directory into which to save associated files.
michael@0 397 filesFolder = persistArgs.targetFile.clone();
michael@0 398
michael@0 399 var nameWithoutExtension = getFileBaseName(filesFolder.leafName);
michael@0 400 var filesFolderLeafName =
michael@0 401 ContentAreaUtils.stringBundle
michael@0 402 .formatStringFromName("filesFolder", [nameWithoutExtension], 1);
michael@0 403
michael@0 404 filesFolder.leafName = filesFolderLeafName;
michael@0 405 }
michael@0 406
michael@0 407 var encodingFlags = 0;
michael@0 408 if (persistArgs.targetContentType == "text/plain") {
michael@0 409 encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED;
michael@0 410 encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
michael@0 411 encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
michael@0 412 }
michael@0 413 else {
michael@0 414 encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
michael@0 415 }
michael@0 416
michael@0 417 const kWrapColumn = 80;
michael@0 418 persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder,
michael@0 419 persistArgs.targetContentType, encodingFlags, kWrapColumn);
michael@0 420 } else {
michael@0 421 let privacyContext = persistArgs.initiatingWindow
michael@0 422 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
michael@0 423 .getInterface(Components.interfaces.nsIWebNavigation)
michael@0 424 .QueryInterface(Components.interfaces.nsILoadContext);
michael@0 425 persist.saveURI(persistArgs.sourceURI,
michael@0 426 persistArgs.sourceCacheKey, persistArgs.sourceReferrer, persistArgs.sourcePostData, null,
michael@0 427 targetFileURL, privacyContext);
michael@0 428 }
michael@0 429 }
michael@0 430
michael@0 431 /**
michael@0 432 * Structure for holding info about automatically supplied parameters for
michael@0 433 * internalSave(...). This allows parameters to be supplied so the user does not
michael@0 434 * need to be prompted for file info.
michael@0 435 * @param aFileAutoChosen This is an nsIFile object that has been
michael@0 436 * pre-determined as the filename for the target to save to
michael@0 437 * @param aUriAutoChosen This is the nsIURI object for the target
michael@0 438 */
michael@0 439 function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
michael@0 440 this.file = aFileAutoChosen;
michael@0 441 this.uri = aUriAutoChosen;
michael@0 442 }
michael@0 443
michael@0 444 /**
michael@0 445 * Structure for holding info about a URL and the target filename it should be
michael@0 446 * saved to. This structure is populated by initFileInfo(...).
michael@0 447 * @param aSuggestedFileName This is used by initFileInfo(...) when it
michael@0 448 * cannot 'discover' the filename from the url
michael@0 449 * @param aFileName The target filename
michael@0 450 * @param aFileBaseName The filename without the file extension
michael@0 451 * @param aFileExt The extension of the filename
michael@0 452 * @param aUri An nsIURI object for the url that is being saved
michael@0 453 */
michael@0 454 function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) {
michael@0 455 this.suggestedFileName = aSuggestedFileName;
michael@0 456 this.fileName = aFileName;
michael@0 457 this.fileBaseName = aFileBaseName;
michael@0 458 this.fileExt = aFileExt;
michael@0 459 this.uri = aUri;
michael@0 460 }
michael@0 461
michael@0 462 /**
michael@0 463 * Determine what the 'default' filename string is, its file extension and the
michael@0 464 * filename without the extension. This filename is used when prompting the user
michael@0 465 * for confirmation in the file picker dialog.
michael@0 466 * @param aFI A FileInfo structure into which we'll put the results of this method.
michael@0 467 * @param aURL The String representation of the URL of the document being saved
michael@0 468 * @param aURLCharset The charset of aURL.
michael@0 469 * @param aDocument The document to be saved
michael@0 470 * @param aContentType The content type we're saving, if it could be
michael@0 471 * determined by the caller.
michael@0 472 * @param aContentDisposition The content-disposition header for the object
michael@0 473 * we're saving, if it could be determined by the caller.
michael@0 474 */
michael@0 475 function initFileInfo(aFI, aURL, aURLCharset, aDocument,
michael@0 476 aContentType, aContentDisposition)
michael@0 477 {
michael@0 478 try {
michael@0 479 // Get an nsIURI object from aURL if possible:
michael@0 480 try {
michael@0 481 aFI.uri = makeURI(aURL, aURLCharset);
michael@0 482 // Assuming nsiUri is valid, calling QueryInterface(...) on it will
michael@0 483 // populate extra object fields (eg filename and file extension).
michael@0 484 var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL);
michael@0 485 aFI.fileExt = url.fileExtension;
michael@0 486 } catch (e) {
michael@0 487 }
michael@0 488
michael@0 489 // Get the default filename:
michael@0 490 aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName),
michael@0 491 aFI.uri, aDocument, aContentDisposition);
michael@0 492 // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied
michael@0 493 // if saveURL(...) was the original caller (hence both aContentType and
michael@0 494 // aDocument are blank). If they were saving a link to a website then make
michael@0 495 // the extension .htm .
michael@0 496 if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) {
michael@0 497 aFI.fileExt = "htm";
michael@0 498 aFI.fileBaseName = aFI.fileName;
michael@0 499 } else {
michael@0 500 aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType);
michael@0 501 aFI.fileBaseName = getFileBaseName(aFI.fileName);
michael@0 502 }
michael@0 503 } catch (e) {
michael@0 504 }
michael@0 505 }
michael@0 506
michael@0 507 /**
michael@0 508 * Given the Filepicker Parameters (aFpP), show the file picker dialog,
michael@0 509 * prompting the user to confirm (or change) the fileName.
michael@0 510 * @param aFpP
michael@0 511 * A structure (see definition in internalSave(...) method)
michael@0 512 * containing all the data used within this method.
michael@0 513 * @param aSkipPrompt
michael@0 514 * If true, attempt to save the file automatically to the user's default
michael@0 515 * download directory, thus skipping the explicit prompt for a file name,
michael@0 516 * but only if the associated preference is set.
michael@0 517 * If false, don't save the file automatically to the user's
michael@0 518 * default download directory, even if the associated preference
michael@0 519 * is set, but ask for the target explicitly.
michael@0 520 * @param aRelatedURI
michael@0 521 * An nsIURI associated with the download. The last used
michael@0 522 * directory of the picker is retrieved from/stored in the
michael@0 523 * Content Pref Service using this URI.
michael@0 524 * @return Promise
michael@0 525 * @resolve a boolean. When true, it indicates that the file picker dialog
michael@0 526 * is accepted.
michael@0 527 */
michael@0 528 function promiseTargetFile(aFpP, /* optional */ aSkipPrompt, /* optional */ aRelatedURI)
michael@0 529 {
michael@0 530 return Task.spawn(function() {
michael@0 531 let downloadLastDir = new DownloadLastDir(window);
michael@0 532 let prefBranch = Services.prefs.getBranch("browser.download.");
michael@0 533 let useDownloadDir = prefBranch.getBoolPref("useDownloadDir");
michael@0 534
michael@0 535 if (!aSkipPrompt)
michael@0 536 useDownloadDir = false;
michael@0 537
michael@0 538 // Default to the user's default downloads directory configured
michael@0 539 // through download prefs.
michael@0 540 let dirPath = yield Downloads.getPreferredDownloadsDirectory();
michael@0 541 let dirExists = yield OS.File.exists(dirPath);
michael@0 542 let dir = new FileUtils.File(dirPath);
michael@0 543
michael@0 544 if (useDownloadDir && dirExists) {
michael@0 545 dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName,
michael@0 546 aFpP.fileInfo.fileExt));
michael@0 547 aFpP.file = uniqueFile(dir);
michael@0 548 throw new Task.Result(true);
michael@0 549 }
michael@0 550
michael@0 551 // We must prompt for the file name explicitly.
michael@0 552 // If we must prompt because we were asked to...
michael@0 553 let deferred = Promise.defer();
michael@0 554 if (useDownloadDir) {
michael@0 555 // Keep async behavior in both branches
michael@0 556 Services.tm.mainThread.dispatch(function() {
michael@0 557 deferred.resolve(null);
michael@0 558 }, Components.interfaces.nsIThread.DISPATCH_NORMAL);
michael@0 559 } else {
michael@0 560 downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(aFile) {
michael@0 561 deferred.resolve(aFile);
michael@0 562 });
michael@0 563 }
michael@0 564 let file = yield deferred.promise;
michael@0 565 if (file && (yield OS.File.exists(file.path))) {
michael@0 566 dir = file;
michael@0 567 dirExists = true;
michael@0 568 }
michael@0 569
michael@0 570 if (!dirExists) {
michael@0 571 // Default to desktop.
michael@0 572 dir = Services.dirsvc.get("Desk", Components.interfaces.nsIFile);
michael@0 573 }
michael@0 574
michael@0 575 let fp = makeFilePicker();
michael@0 576 let titleKey = aFpP.fpTitleKey || "SaveLinkTitle";
michael@0 577 fp.init(window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
michael@0 578 Components.interfaces.nsIFilePicker.modeSave);
michael@0 579
michael@0 580 fp.displayDirectory = dir;
michael@0 581 fp.defaultExtension = aFpP.fileInfo.fileExt;
michael@0 582 fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName,
michael@0 583 aFpP.fileInfo.fileExt);
michael@0 584 appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt,
michael@0 585 aFpP.saveMode);
michael@0 586
michael@0 587 // The index of the selected filter is only preserved and restored if there's
michael@0 588 // more than one filter in addition to "All Files".
michael@0 589 if (aFpP.saveMode != SAVEMODE_FILEONLY) {
michael@0 590 try {
michael@0 591 fp.filterIndex = prefBranch.getIntPref("save_converter_index");
michael@0 592 }
michael@0 593 catch (e) {
michael@0 594 }
michael@0 595 }
michael@0 596
michael@0 597 let deferComplete = Promise.defer();
michael@0 598 fp.open(function(aResult) {
michael@0 599 deferComplete.resolve(aResult);
michael@0 600 });
michael@0 601 let result = yield deferComplete.promise;
michael@0 602 if (result == Components.interfaces.nsIFilePicker.returnCancel || !fp.file) {
michael@0 603 throw new Task.Result(false);
michael@0 604 }
michael@0 605
michael@0 606 if (aFpP.saveMode != SAVEMODE_FILEONLY)
michael@0 607 prefBranch.setIntPref("save_converter_index", fp.filterIndex);
michael@0 608
michael@0 609 // Do not store the last save directory as a pref inside the private browsing mode
michael@0 610 downloadLastDir.setFile(aRelatedURI, fp.file.parent);
michael@0 611
michael@0 612 fp.file.leafName = validateFileName(fp.file.leafName);
michael@0 613
michael@0 614 aFpP.saveAsType = fp.filterIndex;
michael@0 615 aFpP.file = fp.file;
michael@0 616 aFpP.fileURL = fp.fileURL;
michael@0 617
michael@0 618 throw new Task.Result(true);
michael@0 619 });
michael@0 620 }
michael@0 621
michael@0 622 // Since we're automatically downloading, we don't get the file picker's
michael@0 623 // logic to check for existing files, so we need to do that here.
michael@0 624 //
michael@0 625 // Note - this code is identical to that in
michael@0 626 // mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in
michael@0 627 // If you are updating this code, update that code too! We can't share code
michael@0 628 // here since that code is called in a js component.
michael@0 629 function uniqueFile(aLocalFile)
michael@0 630 {
michael@0 631 var collisionCount = 0;
michael@0 632 while (aLocalFile.exists()) {
michael@0 633 collisionCount++;
michael@0 634 if (collisionCount == 1) {
michael@0 635 // Append "(2)" before the last dot in (or at the end of) the filename
michael@0 636 // special case .ext.gz etc files so we don't wind up with .tar(2).gz
michael@0 637 if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
michael@0 638 aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
michael@0 639 else
michael@0 640 aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
michael@0 641 }
michael@0 642 else {
michael@0 643 // replace the last (n) in the filename with (n+1)
michael@0 644 aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")");
michael@0 645 }
michael@0 646 }
michael@0 647 return aLocalFile;
michael@0 648 }
michael@0 649
michael@0 650 #ifdef MOZ_JSDOWNLOADS
michael@0 651 /**
michael@0 652 * Download a URL using the new jsdownloads API.
michael@0 653 *
michael@0 654 * @param aURL
michael@0 655 * the url to download
michael@0 656 * @param [optional] aFileName
michael@0 657 * the destination file name, if omitted will be obtained from the url.
michael@0 658 * @param aInitiatingDocument
michael@0 659 * The document from which the download was initiated.
michael@0 660 */
michael@0 661 function DownloadURL(aURL, aFileName, aInitiatingDocument) {
michael@0 662 // For private browsing, try to get document out of the most recent browser
michael@0 663 // window, or provide our own if there's no browser window.
michael@0 664 let isPrivate = aInitiatingDocument.defaultView
michael@0 665 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 666 .getInterface(Ci.nsIWebNavigation)
michael@0 667 .QueryInterface(Ci.nsILoadContext)
michael@0 668 .usePrivateBrowsing;
michael@0 669
michael@0 670 let fileInfo = new FileInfo(aFileName);
michael@0 671 initFileInfo(fileInfo, aURL, null, null, null, null);
michael@0 672
michael@0 673 let filepickerParams = {
michael@0 674 fileInfo: fileInfo,
michael@0 675 saveMode: SAVEMODE_FILEONLY
michael@0 676 };
michael@0 677
michael@0 678 Task.spawn(function* () {
michael@0 679 let accepted = yield promiseTargetFile(filepickerParams, true, fileInfo.uri);
michael@0 680 if (!accepted)
michael@0 681 return;
michael@0 682
michael@0 683 let file = filepickerParams.file;
michael@0 684 let download = yield Downloads.createDownload({
michael@0 685 source: { url: aURL, isPrivate: isPrivate },
michael@0 686 target: { path: file.path, partFilePath: file.path + ".part" }
michael@0 687 });
michael@0 688 download.tryToKeepPartialData = true;
michael@0 689 download.start();
michael@0 690
michael@0 691 // Add the download to the list, allowing it to be managed.
michael@0 692 let list = yield Downloads.getList(Downloads.ALL);
michael@0 693 list.add(download);
michael@0 694 }).then(null, Components.utils.reportError);
michael@0 695 }
michael@0 696 #endif
michael@0 697
michael@0 698 // We have no DOM, and can only save the URL as is.
michael@0 699 const SAVEMODE_FILEONLY = 0x00;
michael@0 700 // We have a DOM and can save as complete.
michael@0 701 const SAVEMODE_COMPLETE_DOM = 0x01;
michael@0 702 // We have a DOM which we can serialize as text.
michael@0 703 const SAVEMODE_COMPLETE_TEXT = 0x02;
michael@0 704
michael@0 705 // If we are able to save a complete DOM, the 'save as complete' filter
michael@0 706 // must be the first filter appended. The 'save page only' counterpart
michael@0 707 // must be the second filter appended. And the 'save as complete text'
michael@0 708 // filter must be the third filter appended.
michael@0 709 function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode)
michael@0 710 {
michael@0 711 // The bundle name for saving only a specific content type.
michael@0 712 var bundleName;
michael@0 713 // The corresponding filter string for a specific content type.
michael@0 714 var filterString;
michael@0 715
michael@0 716 // XXX all the cases that are handled explicitly here MUST be handled
michael@0 717 // in GetSaveModeForContentType to return a non-fileonly filter.
michael@0 718 switch (aContentType) {
michael@0 719 case "text/html":
michael@0 720 bundleName = "WebPageHTMLOnlyFilter";
michael@0 721 filterString = "*.htm; *.html";
michael@0 722 break;
michael@0 723
michael@0 724 case "application/xhtml+xml":
michael@0 725 bundleName = "WebPageXHTMLOnlyFilter";
michael@0 726 filterString = "*.xht; *.xhtml";
michael@0 727 break;
michael@0 728
michael@0 729 case "image/svg+xml":
michael@0 730 bundleName = "WebPageSVGOnlyFilter";
michael@0 731 filterString = "*.svg; *.svgz";
michael@0 732 break;
michael@0 733
michael@0 734 case "text/xml":
michael@0 735 case "application/xml":
michael@0 736 bundleName = "WebPageXMLOnlyFilter";
michael@0 737 filterString = "*.xml";
michael@0 738 break;
michael@0 739
michael@0 740 default:
michael@0 741 if (aSaveMode != SAVEMODE_FILEONLY)
michael@0 742 throw "Invalid save mode for type '" + aContentType + "'";
michael@0 743
michael@0 744 var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
michael@0 745 if (mimeInfo) {
michael@0 746
michael@0 747 var extEnumerator = mimeInfo.getFileExtensions();
michael@0 748
michael@0 749 var extString = "";
michael@0 750 while (extEnumerator.hasMore()) {
michael@0 751 var extension = extEnumerator.getNext();
michael@0 752 if (extString)
michael@0 753 extString += "; "; // If adding more than one extension,
michael@0 754 // separate by semi-colon
michael@0 755 extString += "*." + extension;
michael@0 756 }
michael@0 757
michael@0 758 if (extString)
michael@0 759 aFilePicker.appendFilter(mimeInfo.description, extString);
michael@0 760 }
michael@0 761
michael@0 762 break;
michael@0 763 }
michael@0 764
michael@0 765 if (aSaveMode & SAVEMODE_COMPLETE_DOM) {
michael@0 766 aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"),
michael@0 767 filterString);
michael@0 768 // We should always offer a choice to save document only if
michael@0 769 // we allow saving as complete.
michael@0 770 aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName(bundleName),
michael@0 771 filterString);
michael@0 772 }
michael@0 773
michael@0 774 if (aSaveMode & SAVEMODE_COMPLETE_TEXT)
michael@0 775 aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText);
michael@0 776
michael@0 777 // Always append the all files (*) filter
michael@0 778 aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
michael@0 779 }
michael@0 780
michael@0 781 function getPostData(aDocument)
michael@0 782 {
michael@0 783 try {
michael@0 784 // Find the session history entry corresponding to the given document. In
michael@0 785 // the current implementation, nsIWebPageDescriptor.currentDescriptor always
michael@0 786 // returns a session history entry.
michael@0 787 var sessionHistoryEntry =
michael@0 788 aDocument.defaultView
michael@0 789 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
michael@0 790 .getInterface(Components.interfaces.nsIWebNavigation)
michael@0 791 .QueryInterface(Components.interfaces.nsIWebPageDescriptor)
michael@0 792 .currentDescriptor
michael@0 793 .QueryInterface(Components.interfaces.nsISHEntry);
michael@0 794 return sessionHistoryEntry.postData;
michael@0 795 }
michael@0 796 catch (e) {
michael@0 797 }
michael@0 798 return null;
michael@0 799 }
michael@0 800
michael@0 801 function makeWebBrowserPersist()
michael@0 802 {
michael@0 803 const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1";
michael@0 804 const persistIID = Components.interfaces.nsIWebBrowserPersist;
michael@0 805 return Components.classes[persistContractID].createInstance(persistIID);
michael@0 806 }
michael@0 807
michael@0 808 function makeURI(aURL, aOriginCharset, aBaseURI)
michael@0 809 {
michael@0 810 return BrowserUtils.makeURI(aURL, aOriginCharset, aBaseURI);
michael@0 811 }
michael@0 812
michael@0 813 function makeFileURI(aFile)
michael@0 814 {
michael@0 815 return BrowserUtils.makeFileURI(aFile);
michael@0 816 }
michael@0 817
michael@0 818 function makeFilePicker()
michael@0 819 {
michael@0 820 const fpContractID = "@mozilla.org/filepicker;1";
michael@0 821 const fpIID = Components.interfaces.nsIFilePicker;
michael@0 822 return Components.classes[fpContractID].createInstance(fpIID);
michael@0 823 }
michael@0 824
michael@0 825 function getMIMEService()
michael@0 826 {
michael@0 827 const mimeSvcContractID = "@mozilla.org/mime;1";
michael@0 828 const mimeSvcIID = Components.interfaces.nsIMIMEService;
michael@0 829 const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID);
michael@0 830 return mimeSvc;
michael@0 831 }
michael@0 832
michael@0 833 // Given aFileName, find the fileName without the extension on the end.
michael@0 834 function getFileBaseName(aFileName)
michael@0 835 {
michael@0 836 // Remove the file extension from aFileName:
michael@0 837 return aFileName.replace(/\.[^.]*$/, "");
michael@0 838 }
michael@0 839
michael@0 840 function getMIMETypeForURI(aURI)
michael@0 841 {
michael@0 842 try {
michael@0 843 return getMIMEService().getTypeFromURI(aURI);
michael@0 844 }
michael@0 845 catch (e) {
michael@0 846 }
michael@0 847 return null;
michael@0 848 }
michael@0 849
michael@0 850 function getMIMEInfoForType(aMIMEType, aExtension)
michael@0 851 {
michael@0 852 if (aMIMEType || aExtension) {
michael@0 853 try {
michael@0 854 return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
michael@0 855 }
michael@0 856 catch (e) {
michael@0 857 }
michael@0 858 }
michael@0 859 return null;
michael@0 860 }
michael@0 861
michael@0 862 function getDefaultFileName(aDefaultFileName, aURI, aDocument,
michael@0 863 aContentDisposition)
michael@0 864 {
michael@0 865 // 1) look for a filename in the content-disposition header, if any
michael@0 866 if (aContentDisposition) {
michael@0 867 const mhpContractID = "@mozilla.org/network/mime-hdrparam;1";
michael@0 868 const mhpIID = Components.interfaces.nsIMIMEHeaderParam;
michael@0 869 const mhp = Components.classes[mhpContractID].getService(mhpIID);
michael@0 870 var dummy = { value: null }; // Need an out param...
michael@0 871 var charset = getCharsetforSave(aDocument);
michael@0 872
michael@0 873 var fileName = null;
michael@0 874 try {
michael@0 875 fileName = mhp.getParameter(aContentDisposition, "filename", charset,
michael@0 876 true, dummy);
michael@0 877 }
michael@0 878 catch (e) {
michael@0 879 try {
michael@0 880 fileName = mhp.getParameter(aContentDisposition, "name", charset, true,
michael@0 881 dummy);
michael@0 882 }
michael@0 883 catch (e) {
michael@0 884 }
michael@0 885 }
michael@0 886 if (fileName)
michael@0 887 return fileName;
michael@0 888 }
michael@0 889
michael@0 890 let docTitle;
michael@0 891 if (aDocument) {
michael@0 892 // If the document looks like HTML or XML, try to use its original title.
michael@0 893 docTitle = validateFileName(aDocument.title).trim();
michael@0 894 if (docTitle) {
michael@0 895 let contentType = aDocument.contentType;
michael@0 896 if (contentType == "application/xhtml+xml" ||
michael@0 897 contentType == "application/xml" ||
michael@0 898 contentType == "image/svg+xml" ||
michael@0 899 contentType == "text/html" ||
michael@0 900 contentType == "text/xml") {
michael@0 901 // 2) Use the document title
michael@0 902 return docTitle;
michael@0 903 }
michael@0 904 }
michael@0 905 }
michael@0 906
michael@0 907 try {
michael@0 908 var url = aURI.QueryInterface(Components.interfaces.nsIURL);
michael@0 909 if (url.fileName != "") {
michael@0 910 // 3) Use the actual file name, if present
michael@0 911 var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
michael@0 912 .getService(Components.interfaces.nsITextToSubURI);
michael@0 913 return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName));
michael@0 914 }
michael@0 915 } catch (e) {
michael@0 916 // This is something like a data: and so forth URI... no filename here.
michael@0 917 }
michael@0 918
michael@0 919 if (docTitle)
michael@0 920 // 4) Use the document title
michael@0 921 return docTitle;
michael@0 922
michael@0 923 if (aDefaultFileName)
michael@0 924 // 5) Use the caller-provided name, if any
michael@0 925 return validateFileName(aDefaultFileName);
michael@0 926
michael@0 927 // 6) If this is a directory, use the last directory name
michael@0 928 var path = aURI.path.match(/\/([^\/]+)\/$/);
michael@0 929 if (path && path.length > 1)
michael@0 930 return validateFileName(path[1]);
michael@0 931
michael@0 932 try {
michael@0 933 if (aURI.host)
michael@0 934 // 7) Use the host.
michael@0 935 return aURI.host;
michael@0 936 } catch (e) {
michael@0 937 // Some files have no information at all, like Javascript generated pages
michael@0 938 }
michael@0 939 try {
michael@0 940 // 8) Use the default file name
michael@0 941 return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName");
michael@0 942 } catch (e) {
michael@0 943 //in case localized string cannot be found
michael@0 944 }
michael@0 945 // 9) If all else fails, use "index"
michael@0 946 return "index";
michael@0 947 }
michael@0 948
michael@0 949 function validateFileName(aFileName)
michael@0 950 {
michael@0 951 var re = /[\/]+/g;
michael@0 952 if (navigator.appVersion.indexOf("Windows") != -1) {
michael@0 953 re = /[\\\/\|]+/g;
michael@0 954 aFileName = aFileName.replace(/[\"]+/g, "'");
michael@0 955 aFileName = aFileName.replace(/[\*\:\?]+/g, " ");
michael@0 956 aFileName = aFileName.replace(/[\<]+/g, "(");
michael@0 957 aFileName = aFileName.replace(/[\>]+/g, ")");
michael@0 958 }
michael@0 959 else if (navigator.appVersion.indexOf("Macintosh") != -1)
michael@0 960 re = /[\:\/]+/g;
michael@0 961 else if (navigator.appVersion.indexOf("Android") != -1) {
michael@0 962 // On mobile devices, the filesystem may be very limited in what
michael@0 963 // it considers valid characters. To avoid errors, we sanitize
michael@0 964 // conservatively.
michael@0 965 const dangerousChars = "*?<>|\":/\\[];,+=";
michael@0 966 var processed = "";
michael@0 967 for (var i = 0; i < aFileName.length; i++)
michael@0 968 processed += aFileName.charCodeAt(i) >= 32 &&
michael@0 969 !(dangerousChars.indexOf(aFileName[i]) >= 0) ? aFileName[i]
michael@0 970 : "_";
michael@0 971
michael@0 972 // Last character should not be a space
michael@0 973 processed = processed.trim();
michael@0 974
michael@0 975 // If a large part of the filename has been sanitized, then we
michael@0 976 // will use a default filename instead
michael@0 977 if (processed.replace(/_/g, "").length <= processed.length/2) {
michael@0 978 // We purposefully do not use a localized default filename,
michael@0 979 // which we could have done using
michael@0 980 // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName")
michael@0 981 // since it may contain invalid characters.
michael@0 982 var original = processed;
michael@0 983 processed = "download";
michael@0 984
michael@0 985 // Preserve a suffix, if there is one
michael@0 986 if (original.indexOf(".") >= 0) {
michael@0 987 var suffix = original.split(".").slice(-1)[0];
michael@0 988 if (suffix && suffix.indexOf("_") < 0)
michael@0 989 processed += "." + suffix;
michael@0 990 }
michael@0 991 }
michael@0 992 return processed;
michael@0 993 }
michael@0 994
michael@0 995 return aFileName.replace(re, "_");
michael@0 996 }
michael@0 997
michael@0 998 function getNormalizedLeafName(aFile, aDefaultExtension)
michael@0 999 {
michael@0 1000 if (!aDefaultExtension)
michael@0 1001 return aFile;
michael@0 1002
michael@0 1003 #ifdef XP_WIN
michael@0 1004 // Remove trailing dots and spaces on windows
michael@0 1005 aFile = aFile.replace(/[\s.]+$/, "");
michael@0 1006 #endif
michael@0 1007
michael@0 1008 // Remove leading dots
michael@0 1009 aFile = aFile.replace(/^\.+/, "");
michael@0 1010
michael@0 1011 // Fix up the file name we're saving to to include the default extension
michael@0 1012 var i = aFile.lastIndexOf(".");
michael@0 1013 if (aFile.substr(i + 1) != aDefaultExtension)
michael@0 1014 return aFile + "." + aDefaultExtension;
michael@0 1015
michael@0 1016 return aFile;
michael@0 1017 }
michael@0 1018
michael@0 1019 function getDefaultExtension(aFilename, aURI, aContentType)
michael@0 1020 {
michael@0 1021 if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp")
michael@0 1022 return ""; // temporary fix for bug 120327
michael@0 1023
michael@0 1024 // First try the extension from the filename
michael@0 1025 const stdURLContractID = "@mozilla.org/network/standard-url;1";
michael@0 1026 const stdURLIID = Components.interfaces.nsIURL;
michael@0 1027 var url = Components.classes[stdURLContractID].createInstance(stdURLIID);
michael@0 1028 url.filePath = aFilename;
michael@0 1029
michael@0 1030 var ext = url.fileExtension;
michael@0 1031
michael@0 1032 // This mirrors some code in nsExternalHelperAppService::DoContent
michael@0 1033 // Use the filename first and then the URI if that fails
michael@0 1034
michael@0 1035 var mimeInfo = getMIMEInfoForType(aContentType, ext);
michael@0 1036
michael@0 1037 if (ext && mimeInfo && mimeInfo.extensionExists(ext))
michael@0 1038 return ext;
michael@0 1039
michael@0 1040 // Well, that failed. Now try the extension from the URI
michael@0 1041 var urlext;
michael@0 1042 try {
michael@0 1043 url = aURI.QueryInterface(Components.interfaces.nsIURL);
michael@0 1044 urlext = url.fileExtension;
michael@0 1045 } catch (e) {
michael@0 1046 }
michael@0 1047
michael@0 1048 if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) {
michael@0 1049 return urlext;
michael@0 1050 }
michael@0 1051 else {
michael@0 1052 try {
michael@0 1053 if (mimeInfo)
michael@0 1054 return mimeInfo.primaryExtension;
michael@0 1055 }
michael@0 1056 catch (e) {
michael@0 1057 }
michael@0 1058 // Fall back on the extensions in the filename and URI for lack
michael@0 1059 // of anything better.
michael@0 1060 return ext || urlext;
michael@0 1061 }
michael@0 1062 }
michael@0 1063
michael@0 1064 function GetSaveModeForContentType(aContentType, aDocument)
michael@0 1065 {
michael@0 1066 // We can only save a complete page if we have a loaded document
michael@0 1067 if (!aDocument)
michael@0 1068 return SAVEMODE_FILEONLY;
michael@0 1069
michael@0 1070 // Find the possible save modes using the provided content type
michael@0 1071 var saveMode = SAVEMODE_FILEONLY;
michael@0 1072 switch (aContentType) {
michael@0 1073 case "text/html":
michael@0 1074 case "application/xhtml+xml":
michael@0 1075 case "image/svg+xml":
michael@0 1076 saveMode |= SAVEMODE_COMPLETE_TEXT;
michael@0 1077 // Fall through
michael@0 1078 case "text/xml":
michael@0 1079 case "application/xml":
michael@0 1080 saveMode |= SAVEMODE_COMPLETE_DOM;
michael@0 1081 break;
michael@0 1082 }
michael@0 1083
michael@0 1084 return saveMode;
michael@0 1085 }
michael@0 1086
michael@0 1087 function getCharsetforSave(aDocument)
michael@0 1088 {
michael@0 1089 if (aDocument)
michael@0 1090 return aDocument.characterSet;
michael@0 1091
michael@0 1092 if (document.commandDispatcher.focusedWindow)
michael@0 1093 return document.commandDispatcher.focusedWindow.document.characterSet;
michael@0 1094
michael@0 1095 return window.content.document.characterSet;
michael@0 1096 }
michael@0 1097
michael@0 1098 /**
michael@0 1099 * Open a URL from chrome, determining if we can handle it internally or need to
michael@0 1100 * launch an external application to handle it.
michael@0 1101 * @param aURL The URL to be opened
michael@0 1102 */
michael@0 1103 function openURL(aURL)
michael@0 1104 {
michael@0 1105 var uri = makeURI(aURL);
michael@0 1106
michael@0 1107 var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
michael@0 1108 .getService(Components.interfaces.nsIExternalProtocolService);
michael@0 1109
michael@0 1110 if (!protocolSvc.isExposedProtocol(uri.scheme)) {
michael@0 1111 // If we're not a browser, use the external protocol service to load the URI.
michael@0 1112 protocolSvc.loadUrl(uri);
michael@0 1113 }
michael@0 1114 else {
michael@0 1115 var recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
michael@0 1116 if (recentWindow) {
michael@0 1117 recentWindow.openUILinkIn(uri.spec, "tab");
michael@0 1118 return;
michael@0 1119 }
michael@0 1120
michael@0 1121 var loadgroup = Components.classes["@mozilla.org/network/load-group;1"]
michael@0 1122 .createInstance(Components.interfaces.nsILoadGroup);
michael@0 1123 var appstartup = Services.startup;
michael@0 1124
michael@0 1125 var loadListener = {
michael@0 1126 onStartRequest: function ll_start(aRequest, aContext) {
michael@0 1127 appstartup.enterLastWindowClosingSurvivalArea();
michael@0 1128 },
michael@0 1129 onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) {
michael@0 1130 appstartup.exitLastWindowClosingSurvivalArea();
michael@0 1131 },
michael@0 1132 QueryInterface: function ll_QI(iid) {
michael@0 1133 if (iid.equals(Components.interfaces.nsISupports) ||
michael@0 1134 iid.equals(Components.interfaces.nsIRequestObserver) ||
michael@0 1135 iid.equals(Components.interfaces.nsISupportsWeakReference))
michael@0 1136 return this;
michael@0 1137 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 1138 }
michael@0 1139 }
michael@0 1140 loadgroup.groupObserver = loadListener;
michael@0 1141
michael@0 1142 var uriListener = {
michael@0 1143 onStartURIOpen: function(uri) { return false; },
michael@0 1144 doContent: function(ctype, preferred, request, handler) { return false; },
michael@0 1145 isPreferred: function(ctype, desired) { return false; },
michael@0 1146 canHandleContent: function(ctype, preferred, desired) { return false; },
michael@0 1147 loadCookie: null,
michael@0 1148 parentContentListener: null,
michael@0 1149 getInterface: function(iid) {
michael@0 1150 if (iid.equals(Components.interfaces.nsIURIContentListener))
michael@0 1151 return this;
michael@0 1152 if (iid.equals(Components.interfaces.nsILoadGroup))
michael@0 1153 return loadgroup;
michael@0 1154 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 1155 }
michael@0 1156 }
michael@0 1157
michael@0 1158 var channel = Services.io.newChannelFromURI(uri);
michael@0 1159 var uriLoader = Components.classes["@mozilla.org/uriloader;1"]
michael@0 1160 .getService(Components.interfaces.nsIURILoader);
michael@0 1161 uriLoader.openURI(channel,
michael@0 1162 Components.interfaces.nsIURILoader.IS_CONTENT_PREFERRED,
michael@0 1163 uriListener);
michael@0 1164 }
michael@0 1165 }

mercurial