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