toolkit/content/contentAreaUtils.js

changeset 0
6474c204b198
     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 +}

mercurial